Browse Source
* added customer cache && refactored assign/unassing to/from customer nodes * refactoring of customer action nodes & added common tests * added tests for upgrade * updated controller & service tests for customer api * replaced findCustomerByTenantIdAndTitle with cacheable method * add new line to end of ../upgrade/3.6.2/schema_update.sql file * added cache for find user by tenant id and email & relation action nodes refactoring * fixed synchronization issue & refactored logic & refactored node upgrade tests * fixed testSaveUserWithSameEmail test in the UserControllerTest. Removed empty space & added exclamation mark * updated tests for create relation node && added two tests for delete relation node * replaced Lists with EnumSets where it was possible & replaced MethodSource with EnumSource where it was possible & added test for upgrade for both create and delete relation nodes * updated upgrade tests & added tests for delete relation node * refactoring * updated create & delete relation nodes descriptions * updated default config for delete relation node * minor improvements for custom action nodes after self review * added upgrade for customers with the same titles * replaced parameterized test annotation when deleteForSingleEntity is false * fix typo in reSaveCustomerWithNewTitle upgrade logic * fixes after review & additional improvements * fixed testAssignAndUnassignEntityViewToCustomer test * updated upgrade logic & changed logic of public customers lookup * fixed dao method & added test * renamed method to findPublicCustomerByTenantId * fix typo in the find public customer query * added @Serial annotation for serialVersionUID & rollback saveAndFlush to save in UserServiceImpl * fixes to async find or create logic * fixed after review * fix typo * set Transactional annotation to findOrCreatePublicCustomerpull/10527/head
committed by
GitHub
52 changed files with 3115 additions and 854 deletions
@ -0,0 +1,30 @@ |
|||||
|
/** |
||||
|
* 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.cache.customer; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
@Data |
||||
|
@RequiredArgsConstructor |
||||
|
public class CustomerCacheEvictEvent { |
||||
|
|
||||
|
private final TenantId tenantId; |
||||
|
private final String newTitle; |
||||
|
private final String oldTitle; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
/** |
||||
|
* 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.cache.customer; |
||||
|
|
||||
|
import lombok.Builder; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import lombok.Getter; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
import java.io.Serial; |
||||
|
import java.io.Serializable; |
||||
|
|
||||
|
@Getter |
||||
|
@EqualsAndHashCode |
||||
|
@RequiredArgsConstructor |
||||
|
@Builder |
||||
|
public class CustomerCacheKey implements Serializable { |
||||
|
|
||||
|
@Serial |
||||
|
private static final long serialVersionUID = 5706958428811356925L; |
||||
|
|
||||
|
private final TenantId tenantId; |
||||
|
private final String title; |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return tenantId + "_" + title; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* 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.cache.customer; |
||||
|
|
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.cache.CacheManager; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.cache.CaffeineTbTransactionalCache; |
||||
|
import org.thingsboard.server.common.data.CacheConstants; |
||||
|
import org.thingsboard.server.common.data.Customer; |
||||
|
|
||||
|
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) |
||||
|
@Service("CustomerCache") |
||||
|
public class CustomerCaffeineCache extends CaffeineTbTransactionalCache<CustomerCacheKey, Customer> { |
||||
|
|
||||
|
public CustomerCaffeineCache(CacheManager cacheManager) { |
||||
|
super(cacheManager, CacheConstants.CUSTOMER_CACHE); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
/** |
||||
|
* 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.cache.customer; |
||||
|
|
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.cache.CacheSpecsMap; |
||||
|
import org.thingsboard.server.cache.RedisTbTransactionalCache; |
||||
|
import org.thingsboard.server.cache.TBRedisCacheConfiguration; |
||||
|
import org.thingsboard.server.cache.TbJsonRedisSerializer; |
||||
|
import org.thingsboard.server.common.data.CacheConstants; |
||||
|
import org.thingsboard.server.common.data.Customer; |
||||
|
|
||||
|
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") |
||||
|
@Service("CustomerCache") |
||||
|
public class CustomerRedisCache extends RedisTbTransactionalCache<CustomerCacheKey, Customer> { |
||||
|
|
||||
|
public CustomerRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { |
||||
|
super(CacheConstants.CUSTOMER_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(Customer.class)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
/** |
||||
|
* 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.cache.user; |
||||
|
|
||||
|
import lombok.Builder; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import lombok.Getter; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
import java.io.Serial; |
||||
|
import java.io.Serializable; |
||||
|
|
||||
|
@Getter |
||||
|
@EqualsAndHashCode |
||||
|
@RequiredArgsConstructor |
||||
|
@Builder |
||||
|
public class UserCacheKey implements Serializable { |
||||
|
|
||||
|
@Serial |
||||
|
private static final long serialVersionUID = 7357353074893750678L; |
||||
|
|
||||
|
private final TenantId tenantId; |
||||
|
private final String email; |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return tenantId + "_" + email; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* 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.cache.user; |
||||
|
|
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.cache.CacheManager; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.cache.CaffeineTbTransactionalCache; |
||||
|
import org.thingsboard.server.common.data.CacheConstants; |
||||
|
import org.thingsboard.server.common.data.User; |
||||
|
|
||||
|
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) |
||||
|
@Service("UserCache") |
||||
|
public class UserCaffeineCache extends CaffeineTbTransactionalCache<UserCacheKey, User> { |
||||
|
|
||||
|
public UserCaffeineCache(CacheManager cacheManager) { |
||||
|
super(cacheManager, CacheConstants.USER_CACHE); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
/** |
||||
|
* 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.cache.user; |
||||
|
|
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.cache.CacheSpecsMap; |
||||
|
import org.thingsboard.server.cache.RedisTbTransactionalCache; |
||||
|
import org.thingsboard.server.cache.TBRedisCacheConfiguration; |
||||
|
import org.thingsboard.server.cache.TbJsonRedisSerializer; |
||||
|
import org.thingsboard.server.common.data.CacheConstants; |
||||
|
import org.thingsboard.server.common.data.User; |
||||
|
|
||||
|
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") |
||||
|
@Service("UserCache") |
||||
|
public class UserRedisCache extends RedisTbTransactionalCache<UserCacheKey, User> { |
||||
|
|
||||
|
public UserRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { |
||||
|
super(CacheConstants.USER_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(User.class)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,335 @@ |
|||||
|
/** |
||||
|
* 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 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.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.common.util.ListeningExecutor; |
||||
|
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest; |
||||
|
import org.thingsboard.rule.engine.TestDbCallbackExecutor; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNode; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.Customer; |
||||
|
import org.thingsboard.server.common.data.Dashboard; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.EntityView; |
||||
|
import org.thingsboard.server.common.data.StringUtils; |
||||
|
import org.thingsboard.server.common.data.asset.Asset; |
||||
|
import org.thingsboard.server.common.data.edge.Edge; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.DashboardId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EdgeId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityIdFactory; |
||||
|
import org.thingsboard.server.common.data.id.EntityViewId; |
||||
|
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.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
import org.thingsboard.server.dao.asset.AssetService; |
||||
|
import org.thingsboard.server.dao.customer.CustomerService; |
||||
|
import org.thingsboard.server.dao.dashboard.DashboardService; |
||||
|
import org.thingsboard.server.dao.device.DeviceService; |
||||
|
import org.thingsboard.server.dao.edge.EdgeService; |
||||
|
import org.thingsboard.server.dao.entityview.EntityViewService; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
import java.util.EnumSet; |
||||
|
import java.util.Map; |
||||
|
import java.util.Optional; |
||||
|
import java.util.Set; |
||||
|
import java.util.UUID; |
||||
|
import java.util.function.Consumer; |
||||
|
import java.util.stream.Collectors; |
||||
|
import java.util.stream.Stream; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.Mockito.never; |
||||
|
import static org.mockito.Mockito.spy; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
import static org.mockito.Mockito.verifyNoInteractions; |
||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
class TbAssignToCustomerNodeTest extends AbstractRuleNodeUpgradeTest { |
||||
|
|
||||
|
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET, |
||||
|
EntityType.ENTITY_VIEW, EntityType.EDGE, EntityType.DASHBOARD); |
||||
|
|
||||
|
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(", ")); |
||||
|
|
||||
|
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values()) |
||||
|
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet()); |
||||
|
|
||||
|
private final Device DEVICE = new Device(); |
||||
|
private final Asset ASSET = new Asset(); |
||||
|
private final EntityView ENTITY_VIEW = new EntityView(); |
||||
|
private final Edge EDGE = new Edge(); |
||||
|
private final Dashboard DASHBOARD = new Dashboard(); |
||||
|
|
||||
|
private final TenantId TENANT_ID = new TenantId(UUID.fromString("c818385f-e661-407f-8c52-daf2dddf406d")); |
||||
|
private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("c3570bd0-c0bc-4609-97a4-6f57d7c8b809")); |
||||
|
|
||||
|
private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); |
||||
|
|
||||
|
private static Stream<Arguments> givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown() { |
||||
|
return unsupportedEntityTypes.stream().flatMap(type -> Stream.of(Arguments.of(type))); |
||||
|
} |
||||
|
|
||||
|
private static Stream<Arguments> givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify() { |
||||
|
return supportedEntityTypes.stream() |
||||
|
.flatMap(type -> Stream.of(Arguments.of(type, StringUtils.randomAlphabetic(5)))); |
||||
|
} |
||||
|
|
||||
|
private TbAssignToCustomerNode node; |
||||
|
private TbAssignToCustomerNodeConfiguration config; |
||||
|
|
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
|
||||
|
@Mock |
||||
|
private CustomerService customerServiceMock; |
||||
|
|
||||
|
@Mock |
||||
|
private DeviceService deviceServiceMock; |
||||
|
|
||||
|
@Mock |
||||
|
private AssetService assetServiceMock; |
||||
|
|
||||
|
@Mock |
||||
|
private EntityViewService entityViewServiceMock; |
||||
|
|
||||
|
@Mock |
||||
|
private EdgeService edgeServiceMock; |
||||
|
|
||||
|
@Mock |
||||
|
private DashboardService dashboardServiceMock; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() throws TbNodeException { |
||||
|
node = spy(new TbAssignToCustomerNode()); |
||||
|
config = new TbAssignToCustomerNodeConfiguration().defaultConfiguration(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected TbNode getTestNode() { |
||||
|
return node; |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void givenDefaultConfig_whenVerify_thenOK() { |
||||
|
var defaultConfig = new TbAssignToCustomerNodeConfiguration().defaultConfiguration(); |
||||
|
assertThat(defaultConfig.getCustomerNamePattern()).isEmpty(); |
||||
|
assertThat(defaultConfig.isCreateCustomerIfNotExists()).isFalse(); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource |
||||
|
void givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown(EntityType originatorType) { |
||||
|
// GIVEN
|
||||
|
var originator = toOriginator(originatorType); |
||||
|
var msg = getTbMsg(originator); |
||||
|
|
||||
|
// WHEN
|
||||
|
var exception = assertThrows(RuntimeException.class, () -> node.onMsg(ctxMock, msg)); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(exception.getMessage()).isEqualTo("Unsupported originator type '" + originatorType + |
||||
|
"'! Only " + supportedEntityTypesStr + " types are allowed."); |
||||
|
verifyNoInteractions(ctxMock); |
||||
|
verifyNoInteractions(customerServiceMock); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify") |
||||
|
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify(EntityType type, String customerTitle) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock); |
||||
|
|
||||
|
config.setCustomerNamePattern(customerTitle); |
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
var originator = toOriginator(type); |
||||
|
var msg = getTbMsg(originator); |
||||
|
var customer = createCustomer(customerTitle); |
||||
|
|
||||
|
when(customerServiceMock.findCustomerByTenantIdAndTitle(eq(TENANT_ID), eq(customerTitle))).thenReturn(Optional.of(customer)); |
||||
|
Map<EntityType, Consumer<EntityId>> entityTypeToAssignConsumerMap = mockMethodCallsForSupportedTypes(); |
||||
|
entityTypeToAssignConsumerMap.get(type).accept(originator); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
verifyMsgSuccess(msg); |
||||
|
verifyNoMoreInteractions(ctxMock); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify") |
||||
|
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifyCustomerCreatedAndSuccessOutMsg(EntityType type, String customerTitle) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock); |
||||
|
when(ctxMock.getSelfId()).thenReturn(RULE_NODE_ID); |
||||
|
|
||||
|
config.setCreateCustomerIfNotExists(true); |
||||
|
config.setCustomerNamePattern(customerTitle); |
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
var originator = toOriginator(type); |
||||
|
var msg = getTbMsg(originator); |
||||
|
var customer = createCustomer(customerTitle); |
||||
|
|
||||
|
when(customerServiceMock.findCustomerByTenantIdAndTitle(eq(TENANT_ID), eq(customerTitle))).thenReturn(Optional.empty()); |
||||
|
when(customerServiceMock.saveCustomer(any(Customer.class))).thenReturn(customer); |
||||
|
Map<EntityType, Consumer<EntityId>> entityTypeToEntityIdConsumerMap = mockMethodCallsForSupportedTypes(); |
||||
|
entityTypeToEntityIdConsumerMap.get(type).accept(originator); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class); |
||||
|
verify(ctxMock).enqueue(any(), runnableCaptor.capture(), any()); |
||||
|
runnableCaptor.getValue().run(); |
||||
|
verify(ctxMock).customerCreatedMsg(any(), eq(RULE_NODE_ID)); |
||||
|
verifyMsgSuccess(msg); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerify") |
||||
|
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifyCustomerNotFound(EntityType type, String customerTitle) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock); |
||||
|
|
||||
|
config.setCustomerNamePattern(customerTitle); |
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
var originator = toOriginator(type); |
||||
|
var msg = getTbMsg(originator); |
||||
|
|
||||
|
when(customerServiceMock.findCustomerByTenantIdAndTitle(eq(TENANT_ID), eq(customerTitle))).thenReturn(Optional.empty()); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class); |
||||
|
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); |
||||
|
assertThat(throwableCaptor.getValue()).hasMessage("Customer with title '" + customerTitle + "' doesn't exist!"); |
||||
|
|
||||
|
verifyNoMoreInteractions(customerServiceMock); |
||||
|
verifyNoMoreInteractions(ctxMock); |
||||
|
} |
||||
|
|
||||
|
private Map<EntityType, Consumer<EntityId>> mockMethodCallsForSupportedTypes() { |
||||
|
return Map.of( |
||||
|
EntityType.DEVICE, id -> { |
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
when(deviceServiceMock.assignDeviceToCustomer(eq(TENANT_ID), (DeviceId) eq(id), any())) |
||||
|
.thenReturn(DEVICE); |
||||
|
}, |
||||
|
EntityType.ASSET, id -> { |
||||
|
when(ctxMock.getAssetService()).thenReturn(assetServiceMock); |
||||
|
when(assetServiceMock.assignAssetToCustomer(eq(TENANT_ID), (AssetId) eq(id), any())) |
||||
|
.thenReturn(ASSET); |
||||
|
}, |
||||
|
EntityType.ENTITY_VIEW, id -> { |
||||
|
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); |
||||
|
when(entityViewServiceMock.assignEntityViewToCustomer(eq(TENANT_ID), (EntityViewId) eq(id), any())) |
||||
|
.thenReturn(ENTITY_VIEW); |
||||
|
}, |
||||
|
EntityType.EDGE, id -> { |
||||
|
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock); |
||||
|
when(edgeServiceMock.assignEdgeToCustomer(eq(TENANT_ID), (EdgeId) eq(id), any())) |
||||
|
.thenReturn(EDGE); |
||||
|
}, |
||||
|
EntityType.DASHBOARD, id -> { |
||||
|
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock); |
||||
|
when(dashboardServiceMock.assignDashboardToCustomer(eq(TENANT_ID), (DashboardId) eq(id), any())) |
||||
|
.thenReturn(DASHBOARD); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private void verifyMsgSuccess(TbMsg expectedMsg) { |
||||
|
verify(ctxMock).tellSuccess(eq(expectedMsg)); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
} |
||||
|
|
||||
|
private Customer createCustomer(String customerTitle) { |
||||
|
var customer = new Customer(); |
||||
|
customer.setTitle(customerTitle); |
||||
|
customer.setId(new CustomerId(UUID.randomUUID())); |
||||
|
customer.setTenantId(TENANT_ID); |
||||
|
return customer; |
||||
|
} |
||||
|
|
||||
|
private TbMsg getTbMsg(EntityId originator) { |
||||
|
return TbMsg.newMsg(TbMsgType.NA, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
} |
||||
|
|
||||
|
private EntityId toOriginator(EntityType type) { |
||||
|
return EntityIdFactory.getByTypeAndId(type.name(), UUID.randomUUID().toString()); |
||||
|
} |
||||
|
|
||||
|
// Rule nodes upgrade
|
||||
|
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { |
||||
|
return Stream.of( |
||||
|
// default config for version 0
|
||||
|
Arguments.of(0, |
||||
|
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\",\"customerCacheExpiration\":300}", |
||||
|
true, |
||||
|
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\"}"), |
||||
|
// default config for version 1 with upgrade from version 0
|
||||
|
Arguments.of(0, |
||||
|
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\"}", |
||||
|
false, |
||||
|
"{\"customerNamePattern\":\"\",\"createCustomerIfNotExists\":\"false\"}") |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,604 @@ |
|||||
|
/** |
||||
|
* 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.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.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; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.common.util.ListeningExecutor; |
||||
|
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest; |
||||
|
import org.thingsboard.rule.engine.TestDbCallbackExecutor; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNode; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.Customer; |
||||
|
import org.thingsboard.server.common.data.Dashboard; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.EntityView; |
||||
|
import org.thingsboard.server.common.data.Tenant; |
||||
|
import org.thingsboard.server.common.data.User; |
||||
|
import org.thingsboard.server.common.data.asset.Asset; |
||||
|
import org.thingsboard.server.common.data.edge.Edge; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.DashboardId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EdgeId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityViewId; |
||||
|
import org.thingsboard.server.common.data.id.HasId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.msg.TbMsgType; |
||||
|
import org.thingsboard.server.common.data.relation.EntityRelation; |
||||
|
import org.thingsboard.server.common.data.relation.EntitySearchDirection; |
||||
|
import org.thingsboard.server.common.data.relation.RelationTypeGroup; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
import org.thingsboard.server.dao.asset.AssetService; |
||||
|
import org.thingsboard.server.dao.customer.CustomerService; |
||||
|
import org.thingsboard.server.dao.dashboard.DashboardService; |
||||
|
import org.thingsboard.server.dao.device.DeviceService; |
||||
|
import org.thingsboard.server.dao.edge.EdgeService; |
||||
|
import org.thingsboard.server.dao.entityview.EntityViewService; |
||||
|
import org.thingsboard.server.dao.relation.RelationService; |
||||
|
import org.thingsboard.server.dao.user.UserService; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
import java.util.EnumSet; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.NoSuchElementException; |
||||
|
import java.util.Optional; |
||||
|
import java.util.Set; |
||||
|
import java.util.UUID; |
||||
|
import java.util.function.Consumer; |
||||
|
import java.util.stream.Collectors; |
||||
|
import java.util.stream.Stream; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.assertj.core.api.Assertions.assertThatCode; |
||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.anySet; |
||||
|
import static org.mockito.ArgumentMatchers.anyString; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.Mockito.never; |
||||
|
import static org.mockito.Mockito.spy; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
import static org.mockito.Mockito.verifyNoInteractions; |
||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class TbDeleteRelationNodeTest extends AbstractRuleNodeUpgradeTest { |
||||
|
|
||||
|
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.TENANT, EntityType.DEVICE, |
||||
|
EntityType.ASSET, EntityType.CUSTOMER, EntityType.ENTITY_VIEW, EntityType.DASHBOARD, EntityType.EDGE, EntityType.USER); |
||||
|
|
||||
|
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(" ,")); |
||||
|
|
||||
|
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values()) |
||||
|
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet()); |
||||
|
|
||||
|
private static Stream<Arguments> givenSupportedEntityType_whenOnMsg_thenVerifyEntityNotFoundExceptionThrown() { |
||||
|
return supportedEntityTypes.stream().filter(entityType -> !entityType.equals(EntityType.TENANT)).map(Arguments::of); |
||||
|
} |
||||
|
|
||||
|
private static final TenantId tenantId = new TenantId(UUID.fromString("6fdb457d-0910-401c-8880-abc251e6a1e2")); |
||||
|
private static final DeviceId deviceId = new DeviceId(UUID.fromString("4eef91a7-8865-4c3c-837d-ed6f6577508b")); |
||||
|
private static final AssetId assetId = new AssetId(UUID.fromString("f4fd3b10-3f36-4d46-a162-5e62050774cc")); |
||||
|
private static final CustomerId customerId = new CustomerId(UUID.fromString("ab890af2-3622-41e0-ac94-14d50af84348")); |
||||
|
private static final EntityViewId entityViewId = new EntityViewId(UUID.fromString("39ce8d03-52a3-4aa8-b561-267d1d9d68b5")); |
||||
|
private static final EdgeId edgeId = new EdgeId(UUID.fromString("dc4f9809-b6f9-48f9-8057-2737216cfdf7")); |
||||
|
private static final DashboardId dashboardId = new DashboardId(UUID.fromString("fda72baa-c882-4723-9693-25995dc37bc5")); |
||||
|
|
||||
|
private static Stream<Arguments> givenSupportedEntityType_whenOnMsg_thenVerifyConditions() { |
||||
|
return Stream.of( |
||||
|
Arguments.of(new Device(deviceId)), |
||||
|
Arguments.of(new Asset(assetId)), |
||||
|
Arguments.of(new Customer(customerId)), |
||||
|
Arguments.of(new EntityView(entityViewId)), |
||||
|
Arguments.of(new Edge(edgeId)), |
||||
|
Arguments.of(new Dashboard(dashboardId)), |
||||
|
Arguments.of(new Tenant(tenantId)) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private final DeviceId originatorId = new DeviceId(UUID.fromString("574c9840-0885-4d12-be69-f557d7471a78")); |
||||
|
|
||||
|
private final ListeningExecutor dbExecutor = new TestDbCallbackExecutor(); |
||||
|
|
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
@Mock |
||||
|
private AssetService assetServiceMock; |
||||
|
@Mock |
||||
|
private DeviceService deviceServiceMock; |
||||
|
@Mock |
||||
|
private EntityViewService entityViewServiceMock; |
||||
|
@Mock |
||||
|
private CustomerService customerServiceMock; |
||||
|
@Mock |
||||
|
private EdgeService edgeServiceMock; |
||||
|
@Mock |
||||
|
private UserService userServiceMock; |
||||
|
@Mock |
||||
|
private DashboardService dashboardServiceMock; |
||||
|
@Mock |
||||
|
private RelationService relationServiceMock; |
||||
|
|
||||
|
|
||||
|
private TbDeleteRelationNode node; |
||||
|
private TbDeleteRelationNodeConfiguration config; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() throws TbNodeException { |
||||
|
node = spy(new TbDeleteRelationNode()); |
||||
|
config = new TbDeleteRelationNodeConfiguration().defaultConfiguration(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void givenDefaultConfig_whenVerify_thenOK() { |
||||
|
var defaultConfig = new TbDeleteRelationNodeConfiguration().defaultConfiguration(); |
||||
|
assertThat(defaultConfig.getDirection()).isEqualTo(EntitySearchDirection.FROM); |
||||
|
assertThat(defaultConfig.getRelationType()).isEqualTo(EntityRelation.CONTAINS_TYPE); |
||||
|
assertThat(defaultConfig.getEntityNamePattern()).isEqualTo(""); |
||||
|
assertThat(defaultConfig.getEntityTypePattern()).isEqualTo(null); |
||||
|
assertThat(defaultConfig.getEntityType()).isEqualTo(null); |
||||
|
assertThat(defaultConfig.isDeleteForSingleEntity()).isFalse(); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@EnumSource(EntityType.class) |
||||
|
void givenEntityType_whenInit_thenVerifyExceptionThrownIfTypeIsUnsupported(EntityType entityType) { |
||||
|
// GIVEN
|
||||
|
config.setEntityType(entityType); |
||||
|
config.setDeleteForSingleEntity(true); |
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
|
||||
|
// WHEN-THEN
|
||||
|
if (unsupportedEntityTypes.contains(entityType)) { |
||||
|
assertThatThrownBy(() -> node.init(ctxMock, nodeConfiguration)) |
||||
|
.isInstanceOf(TbNodeException.class) |
||||
|
.hasMessage("Unsupported entity type '" + entityType + |
||||
|
"'! Only " + supportedEntityTypesStr + " types are allowed."); |
||||
|
} else { |
||||
|
assertThatCode(() -> node.init(ctxMock, nodeConfiguration)).doesNotThrowAnyException(); |
||||
|
} |
||||
|
verifyNoInteractions(ctxMock); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyEntityNotFoundExceptionThrown") |
||||
|
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyEntityNotFoundExceptionThrown(EntityType entityType) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setEntityType(entityType); |
||||
|
config.setEntityNamePattern("${name}"); |
||||
|
config.setEntityTypePattern("${type}"); |
||||
|
config.setDeleteForSingleEntity(true); |
||||
|
|
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(tenantId); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); |
||||
|
|
||||
|
var mockMethodCallsMap = mockEntityServiceCallsEntityNotFound(); |
||||
|
mockMethodCallsMap.get(entityType).run(); |
||||
|
|
||||
|
var md = getMetadataWithNameTemplate(); |
||||
|
var msg = getTbMsg(originatorId, md); |
||||
|
|
||||
|
// todo fix TestDbCallbackExecutor exception handling.
|
||||
|
switch (entityType) { |
||||
|
case CUSTOMER -> { |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class); |
||||
|
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); |
||||
|
assertThat(throwableCaptor.getValue()) |
||||
|
.isInstanceOf(NoSuchElementException.class) |
||||
|
.hasMessage(EntityType.CUSTOMER.getNormalName() + " with title 'EntityName' doesn't exist!"); |
||||
|
} |
||||
|
case DEVICE, ASSET -> { |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class); |
||||
|
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); |
||||
|
assertThat(throwableCaptor.getValue()) |
||||
|
.isInstanceOf(NoSuchElementException.class) |
||||
|
.hasMessage(entityType.getNormalName() + " with name 'EntityName' doesn't exist!"); |
||||
|
} |
||||
|
default -> assertThatThrownBy(() -> node.onMsg(ctxMock, msg)) |
||||
|
.isInstanceOf(RuntimeException.class).hasCauseInstanceOf(NoSuchElementException.class); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions") |
||||
|
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyRelationDeletedAndOutMsgSuccess(HasId entity) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
var entityId = (EntityId) entity.getId(); |
||||
|
var entityType = entityId.getEntityType(); |
||||
|
|
||||
|
config.setEntityType(entityType); |
||||
|
config.setEntityNamePattern("${name}"); |
||||
|
config.setEntityTypePattern("${type}"); |
||||
|
config.setDeleteForSingleEntity(true); |
||||
|
|
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(tenantId); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); |
||||
|
when(ctxMock.getRelationService()).thenReturn(relationServiceMock); |
||||
|
|
||||
|
var mockMethodCallsMap = mockEntityServiceCalls(); |
||||
|
mockMethodCallsMap.get(entityType).accept(entity); |
||||
|
|
||||
|
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(true)); |
||||
|
when(relationServiceMock.deleteRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(true)); |
||||
|
|
||||
|
var md = getMetadataWithNameTemplate(); |
||||
|
var msg = getTbMsg(originatorId, md); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var verifyMethodCallsMap = verifyEntityServiceCalls(); |
||||
|
verifyMethodCallsMap.get(entityType).accept(entity); |
||||
|
|
||||
|
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)); |
||||
|
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)); |
||||
|
|
||||
|
verify(ctxMock).tellSuccess(eq(msg)); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
verify(ctxMock).getDbCallbackExecutor(); |
||||
|
verifyNoMoreInteractions(ctxMock, relationServiceMock); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions") |
||||
|
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyRelationFailedToDeleteAndOutMsgFailure(HasId entity) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
var entityId = (EntityId) entity.getId(); |
||||
|
var entityType = entityId.getEntityType(); |
||||
|
|
||||
|
config.setEntityType(entityType); |
||||
|
config.setEntityNamePattern("${name}"); |
||||
|
config.setEntityTypePattern("${type}"); |
||||
|
config.setDeleteForSingleEntity(true); |
||||
|
|
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(tenantId); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); |
||||
|
when(ctxMock.getRelationService()).thenReturn(relationServiceMock); |
||||
|
|
||||
|
var mockMethodCallsMap = mockEntityServiceCalls(); |
||||
|
mockMethodCallsMap.get(entityType).accept(entity); |
||||
|
|
||||
|
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(true)); |
||||
|
when(relationServiceMock.deleteRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false)); |
||||
|
|
||||
|
var md = getMetadataWithNameTemplate(); |
||||
|
var msg = getTbMsg(originatorId, md); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var verifyMethodCallsMap = verifyEntityServiceCalls(); |
||||
|
verifyMethodCallsMap.get(entityType).accept(entity); |
||||
|
|
||||
|
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)); |
||||
|
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)); |
||||
|
|
||||
|
var throwableCaptor = ArgumentCaptor.forClass(Throwable.class); |
||||
|
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
verify(ctxMock).getDbCallbackExecutor(); |
||||
|
verifyNoMoreInteractions(ctxMock, relationServiceMock); |
||||
|
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage("Failed to delete relation(s) with originator!"); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource("givenSupportedEntityType_whenOnMsg_thenVerifyConditions") |
||||
|
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsTrue_thenVerifyRelationNotFoundAndOutMsgSuccess(HasId entity) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
var entityId = (EntityId) entity.getId(); |
||||
|
var entityType = entityId.getEntityType(); |
||||
|
|
||||
|
config.setEntityType(entityType); |
||||
|
config.setEntityNamePattern("${name}"); |
||||
|
config.setEntityTypePattern("${type}"); |
||||
|
config.setDeleteForSingleEntity(true); |
||||
|
|
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(tenantId); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); |
||||
|
when(ctxMock.getRelationService()).thenReturn(relationServiceMock); |
||||
|
|
||||
|
var mockMethodCallsMap = mockEntityServiceCalls(); |
||||
|
mockMethodCallsMap.get(entityType).accept(entity); |
||||
|
|
||||
|
when(relationServiceMock.checkRelationAsync(any(), any(), any(), any(), any())).thenReturn(Futures.immediateFuture(false)); |
||||
|
|
||||
|
var md = getMetadataWithNameTemplate(); |
||||
|
var msg = getTbMsg(originatorId, md); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
var verifyMethodCallsMap = verifyEntityServiceCalls(); |
||||
|
verifyMethodCallsMap.get(entityType).accept(entity); |
||||
|
|
||||
|
verify(relationServiceMock).checkRelationAsync(eq(tenantId), eq(originatorId), eq(entityId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)); |
||||
|
|
||||
|
verify(ctxMock).tellSuccess(eq(msg)); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
verify(ctxMock).getDbCallbackExecutor(); |
||||
|
verifyNoMoreInteractions(ctxMock, relationServiceMock); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsFalse_thenVerifyRelationsDeletedAndOutMsgSuccess() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
|
||||
|
config.setEntityType(EntityType.DEVICE); |
||||
|
config.setEntityNamePattern("${name}"); |
||||
|
config.setEntityTypePattern("${type}"); |
||||
|
|
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(tenantId); |
||||
|
when(ctxMock.getRelationService()).thenReturn(relationServiceMock); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); |
||||
|
|
||||
|
var relationToDelete = new EntityRelation(); |
||||
|
when(relationServiceMock.findByFromAndTypeAsync(any(), any(), any(), any())).thenReturn(Futures.immediateFuture(List.of(relationToDelete))); |
||||
|
when(relationServiceMock.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); |
||||
|
|
||||
|
var md = getMetadataWithNameTemplate(); |
||||
|
var msg = getTbMsg(originatorId, md); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
verify(relationServiceMock).findByFromAndTypeAsync(eq(tenantId), eq(originatorId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)); |
||||
|
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(relationToDelete)); |
||||
|
|
||||
|
verify(ctxMock).tellSuccess(eq(msg)); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
verifyNoMoreInteractions(ctxMock, relationServiceMock); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Test |
||||
|
void givenSupportedEntityType_whenOnMsgAndDeleteForSingleEntityIsFalse_thenVerifyRelationFailedToDeleteAndOutMsgFailure() throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
config.setEntityType(EntityType.DEVICE); |
||||
|
config.setEntityNamePattern("${name}"); |
||||
|
config.setEntityTypePattern("${type}"); |
||||
|
|
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(tenantId); |
||||
|
when(ctxMock.getRelationService()).thenReturn(relationServiceMock); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor); |
||||
|
|
||||
|
var relationToDelete = new EntityRelation(); |
||||
|
when(relationServiceMock.findByFromAndTypeAsync(any(), any(), any(), any())).thenReturn(Futures.immediateFuture(List.of(relationToDelete))); |
||||
|
when(relationServiceMock.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(false)); |
||||
|
|
||||
|
var md = getMetadataWithNameTemplate(); |
||||
|
var msg = getTbMsg(originatorId, md); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
verify(relationServiceMock).findByFromAndTypeAsync(eq(tenantId), eq(originatorId), eq(EntityRelation.CONTAINS_TYPE), eq(RelationTypeGroup.COMMON)); |
||||
|
verify(relationServiceMock).deleteRelationAsync(eq(tenantId), eq(relationToDelete)); |
||||
|
|
||||
|
var throwableCaptor = ArgumentCaptor.forClass(Throwable.class); |
||||
|
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anyString()); |
||||
|
verify(ctxMock, never()).tellNext(any(), anySet()); |
||||
|
verify(ctxMock, never()).tellSuccess(any()); |
||||
|
verifyNoMoreInteractions(ctxMock, relationServiceMock); |
||||
|
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class).hasMessage("Failed to delete relation(s) with originator!"); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private Map<EntityType, Runnable> mockEntityServiceCallsEntityNotFound() { |
||||
|
return Map.of( |
||||
|
EntityType.DEVICE, () -> { |
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
when(deviceServiceMock.findDeviceByTenantIdAndName(any(), any())).thenReturn(null); |
||||
|
}, |
||||
|
EntityType.ASSET, () -> { |
||||
|
when(ctxMock.getAssetService()).thenReturn(assetServiceMock); |
||||
|
when(assetServiceMock.findAssetByTenantIdAndName(any(), any())).thenReturn(null); |
||||
|
}, |
||||
|
EntityType.CUSTOMER, () -> { |
||||
|
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock); |
||||
|
when(customerServiceMock.findCustomerByTenantIdAndTitle(any(), any())).thenReturn(Optional.empty()); |
||||
|
}, |
||||
|
EntityType.ENTITY_VIEW, () -> { |
||||
|
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); |
||||
|
when(entityViewServiceMock.findEntityViewByTenantIdAndName(any(), any())).thenReturn(null); |
||||
|
}, |
||||
|
EntityType.EDGE, () -> { |
||||
|
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock); |
||||
|
when(edgeServiceMock.findEdgeByTenantIdAndName(any(), any())).thenReturn(null); |
||||
|
}, |
||||
|
EntityType.USER, () -> { |
||||
|
when(ctxMock.getUserService()).thenReturn(userServiceMock); |
||||
|
when(userServiceMock.findUserByTenantIdAndEmail(any(), any())).thenReturn(null); |
||||
|
}, |
||||
|
EntityType.DASHBOARD, () -> { |
||||
|
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock); |
||||
|
when(dashboardServiceMock.findFirstDashboardInfoByTenantIdAndName(any(), any())).thenReturn(null); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private Map<EntityType, Consumer<HasId>> mockEntityServiceCalls() { |
||||
|
return Map.of( |
||||
|
EntityType.DEVICE, hasId -> { |
||||
|
var device = (Device) hasId; |
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
when(deviceServiceMock.findDeviceByTenantIdAndName(any(), any())).thenReturn(device); |
||||
|
}, |
||||
|
EntityType.ASSET, hasId -> { |
||||
|
var asset = (Asset) hasId; |
||||
|
when(ctxMock.getAssetService()).thenReturn(assetServiceMock); |
||||
|
when(assetServiceMock.findAssetByTenantIdAndName(any(), any())).thenReturn(asset); |
||||
|
}, |
||||
|
EntityType.CUSTOMER, hasId -> { |
||||
|
var customer = (Customer) hasId; |
||||
|
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock); |
||||
|
when(customerServiceMock.findCustomerByTenantIdAndTitle(any(), any())).thenReturn(Optional.ofNullable(customer)); |
||||
|
}, |
||||
|
EntityType.ENTITY_VIEW, hasId -> { |
||||
|
var entityView = (EntityView) hasId; |
||||
|
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); |
||||
|
when(entityViewServiceMock.findEntityViewByTenantIdAndName(any(), any())).thenReturn(entityView); |
||||
|
}, |
||||
|
EntityType.EDGE, hasId -> { |
||||
|
var edge = (Edge) hasId; |
||||
|
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock); |
||||
|
when(edgeServiceMock.findEdgeByTenantIdAndName(any(), any())).thenReturn(edge); |
||||
|
}, |
||||
|
EntityType.USER, hasId -> { |
||||
|
var user = (User) hasId; |
||||
|
when(ctxMock.getUserService()).thenReturn(userServiceMock); |
||||
|
when(userServiceMock.findUserByTenantIdAndEmail(any(), any())).thenReturn(user); |
||||
|
}, |
||||
|
EntityType.DASHBOARD, hasId -> { |
||||
|
var dashboard = (Dashboard) hasId; |
||||
|
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock); |
||||
|
when(dashboardServiceMock.findFirstDashboardInfoByTenantIdAndName(any(), any())).thenReturn(dashboard); |
||||
|
}, |
||||
|
EntityType.TENANT, hasId -> { |
||||
|
// do nothing. tenantId returned by ctxMock.
|
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private Map<EntityType, Consumer<HasId>> verifyEntityServiceCalls() { |
||||
|
return Map.of( |
||||
|
EntityType.DEVICE, hasId -> { |
||||
|
verify(deviceServiceMock).findDeviceByTenantIdAndName(eq(tenantId), eq("EntityName")); |
||||
|
verifyNoMoreInteractions(deviceServiceMock); |
||||
|
}, |
||||
|
EntityType.ASSET, hasId -> { |
||||
|
verify(assetServiceMock).findAssetByTenantIdAndName(eq(tenantId), eq("EntityName")); |
||||
|
verifyNoMoreInteractions(assetServiceMock); |
||||
|
}, |
||||
|
EntityType.CUSTOMER, hasId -> { |
||||
|
verify(customerServiceMock).findCustomerByTenantIdAndTitle(eq(tenantId), eq("EntityName")); |
||||
|
verifyNoMoreInteractions(customerServiceMock); |
||||
|
}, |
||||
|
EntityType.ENTITY_VIEW, hasId -> { |
||||
|
verify(entityViewServiceMock).findEntityViewByTenantIdAndName(eq(tenantId), eq("EntityName")); |
||||
|
verifyNoMoreInteractions(entityViewServiceMock); |
||||
|
}, |
||||
|
EntityType.EDGE, hasId -> { |
||||
|
verify(edgeServiceMock).findEdgeByTenantIdAndName(eq(tenantId), eq("EntityName")); |
||||
|
verifyNoMoreInteractions(edgeServiceMock); |
||||
|
}, |
||||
|
EntityType.USER, hasId -> { |
||||
|
verify(userServiceMock).findUserByTenantIdAndEmail(eq(tenantId), eq("EntityName")); |
||||
|
verifyNoMoreInteractions(userServiceMock); |
||||
|
}, |
||||
|
EntityType.DASHBOARD, hasId -> { |
||||
|
verify(dashboardServiceMock).findFirstDashboardInfoByTenantIdAndName(eq(tenantId), eq("EntityName")); |
||||
|
verifyNoMoreInteractions(dashboardServiceMock); |
||||
|
}, |
||||
|
EntityType.TENANT, hasId -> { |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private TbMsg getTbMsg(EntityId originator, TbMsgMetaData metaData) { |
||||
|
return TbMsg.newMsg(TbMsgType.NA, originator, metaData, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
} |
||||
|
|
||||
|
private TbMsgMetaData getMetadataWithNameTemplate() { |
||||
|
var metaData = new TbMsgMetaData(); |
||||
|
metaData.putValue("name", "EntityName"); |
||||
|
return metaData; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
protected TbNode getTestNode() { |
||||
|
return node; |
||||
|
} |
||||
|
|
||||
|
// Rule nodes upgrade
|
||||
|
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { |
||||
|
return Stream.of( |
||||
|
// version 0 config, FROM direction.
|
||||
|
Arguments.of(0, |
||||
|
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," + |
||||
|
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\",\"entityCacheExpiration\":300}", |
||||
|
true, |
||||
|
"{\"deleteForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"DEVICE\"," + |
||||
|
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}"), |
||||
|
// version 0 config, TO direction.
|
||||
|
Arguments.of(0, |
||||
|
"{\"deleteForSingleEntity\":true,\"direction\":\"TO\",\"entityType\":\"DEVICE\"," + |
||||
|
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\",\"entityCacheExpiration\":300}", |
||||
|
true, |
||||
|
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," + |
||||
|
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}"), |
||||
|
// config for version 1 with upgrade from version 0
|
||||
|
Arguments.of(0, |
||||
|
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," + |
||||
|
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}", |
||||
|
false, |
||||
|
"{\"deleteForSingleEntity\":true,\"direction\":\"FROM\",\"entityType\":\"DEVICE\"," + |
||||
|
"\"entityNamePattern\":\"$[name]\",\"relationType\":\"Contains\"}") |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,313 @@ |
|||||
|
/** |
||||
|
* 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 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.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.common.util.ListeningExecutor; |
||||
|
import org.thingsboard.rule.engine.AbstractRuleNodeUpgradeTest; |
||||
|
import org.thingsboard.rule.engine.TestDbCallbackExecutor; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNode; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.Customer; |
||||
|
import org.thingsboard.server.common.data.Dashboard; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.EntityView; |
||||
|
import org.thingsboard.server.common.data.StringUtils; |
||||
|
import org.thingsboard.server.common.data.asset.Asset; |
||||
|
import org.thingsboard.server.common.data.edge.Edge; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.CustomerId; |
||||
|
import org.thingsboard.server.common.data.id.DashboardId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EdgeId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityIdFactory; |
||||
|
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.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
import org.thingsboard.server.dao.asset.AssetService; |
||||
|
import org.thingsboard.server.dao.customer.CustomerService; |
||||
|
import org.thingsboard.server.dao.dashboard.DashboardService; |
||||
|
import org.thingsboard.server.dao.device.DeviceService; |
||||
|
import org.thingsboard.server.dao.edge.EdgeService; |
||||
|
import org.thingsboard.server.dao.entityview.EntityViewService; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
import java.util.EnumSet; |
||||
|
import java.util.Map; |
||||
|
import java.util.Optional; |
||||
|
import java.util.Set; |
||||
|
import java.util.UUID; |
||||
|
import java.util.function.Consumer; |
||||
|
import java.util.stream.Collectors; |
||||
|
import java.util.stream.Stream; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.Mockito.never; |
||||
|
import static org.mockito.Mockito.spy; |
||||
|
import static org.mockito.Mockito.verify; |
||||
|
import static org.mockito.Mockito.verifyNoInteractions; |
||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
class TbUnassignFromCustomerNodeTest extends AbstractRuleNodeUpgradeTest { |
||||
|
|
||||
|
private static final Set<EntityType> supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET, |
||||
|
EntityType.ENTITY_VIEW, EntityType.EDGE, EntityType.DASHBOARD); |
||||
|
|
||||
|
private static final String supportedEntityTypesStr = supportedEntityTypes.stream().map(Enum::name).collect(Collectors.joining(", ")); |
||||
|
|
||||
|
private static final Set<EntityType> unsupportedEntityTypes = Arrays.stream(EntityType.values()) |
||||
|
.filter(type -> !supportedEntityTypes.contains(type)).collect(Collectors.toUnmodifiableSet()); |
||||
|
|
||||
|
private final Device DEVICE = new Device(); |
||||
|
private final Asset ASSET = new Asset(); |
||||
|
private final EntityView ENTITY_VIEW = new EntityView(); |
||||
|
private final Edge EDGE = new Edge(); |
||||
|
private final Dashboard DASHBOARD = new Dashboard(); |
||||
|
|
||||
|
private final TenantId TENANT_ID = new TenantId(UUID.fromString("06fcc15f-2677-436d-a1cb-7754bd0bcccf")); |
||||
|
|
||||
|
private final ListeningExecutor DB_EXECUTOR = new TestDbCallbackExecutor(); |
||||
|
|
||||
|
private static Stream<Arguments> givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown() { |
||||
|
return unsupportedEntityTypes.stream().flatMap(type -> Stream.of(Arguments.of(type))); |
||||
|
} |
||||
|
|
||||
|
private static Stream<Arguments> givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg() { |
||||
|
return supportedEntityTypes.stream() |
||||
|
.flatMap(type -> Stream.of(Arguments.of(type, StringUtils.randomAlphabetic(5)))); |
||||
|
} |
||||
|
|
||||
|
private TbUnassignFromCustomerNode node; |
||||
|
private TbUnassignFromCustomerNodeConfiguration config; |
||||
|
|
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
|
||||
|
@Mock |
||||
|
private CustomerService customerServiceMock; |
||||
|
|
||||
|
@Mock |
||||
|
private DeviceService deviceServiceMock; |
||||
|
|
||||
|
@Mock |
||||
|
private AssetService assetServiceMock; |
||||
|
|
||||
|
@Mock |
||||
|
private EntityViewService entityViewServiceMock; |
||||
|
|
||||
|
@Mock |
||||
|
private EdgeService edgeServiceMock; |
||||
|
|
||||
|
@Mock |
||||
|
private DashboardService dashboardServiceMock; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() throws TbNodeException { |
||||
|
node = spy(new TbUnassignFromCustomerNode()); |
||||
|
config = new TbUnassignFromCustomerNodeConfiguration().defaultConfiguration(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected TbNode getTestNode() { |
||||
|
return node; |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void givenDefaultConfig_whenVerify_thenOK() { |
||||
|
var defaultConfig = new TbUnassignFromCustomerNodeConfiguration().defaultConfiguration(); |
||||
|
assertThat(defaultConfig.getCustomerNamePattern()).isEmpty(); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource |
||||
|
void givenUnsupportedOriginatorType_whenOnMsg_thenVerifyExceptionThrown(EntityType originatorType) { |
||||
|
// GIVEN
|
||||
|
var originator = toOriginator(originatorType); |
||||
|
var msg = getTbMsg(originator); |
||||
|
|
||||
|
// WHEN
|
||||
|
var exception = assertThrows(RuntimeException.class, () -> node.onMsg(ctxMock, msg)); |
||||
|
|
||||
|
// THEN
|
||||
|
assertThat(exception.getMessage()).isEqualTo("Unsupported originator type '" + originatorType + |
||||
|
"'! Only " + supportedEntityTypesStr + " types are allowed."); |
||||
|
verifyNoInteractions(ctxMock); |
||||
|
verifyNoInteractions(customerServiceMock); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg") |
||||
|
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg(EntityType type, String customerTitle) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
config.setCustomerNamePattern(customerTitle); |
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
var originator = toOriginator(type); |
||||
|
var msg = getTbMsg(originator); |
||||
|
|
||||
|
// we search for the customer only if incoming message originator is dashboard.
|
||||
|
if (type.equals(EntityType.DASHBOARD)) { |
||||
|
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock); |
||||
|
var customer = createCustomer(customerTitle); |
||||
|
when(customerServiceMock.findCustomerByTenantIdAndTitle(eq(TENANT_ID), eq(customerTitle))).thenReturn(Optional.of(customer)); |
||||
|
} |
||||
|
Map<EntityType, Consumer<EntityId>> entityTypeToEntityIdConsumerMap = mockMethodCallsForSupportedTypes(); |
||||
|
entityTypeToEntityIdConsumerMap.get(type).accept(originator); |
||||
|
|
||||
|
// WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// THEN
|
||||
|
verifyMsgSuccess(msg); |
||||
|
verifyNoMoreInteractions(ctxMock); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource("givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifySuccessOutMsg") |
||||
|
void givenSupportedOriginatorTypeAndCustomerTitle_whenOnMsg_thenVerifyCustomerSearchedAndNotFoundOnlyForDashboardOriginator(EntityType type, String customerTitle) throws TbNodeException { |
||||
|
// GIVEN
|
||||
|
|
||||
|
when(ctxMock.getTenantId()).thenReturn(TENANT_ID); |
||||
|
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR); |
||||
|
|
||||
|
config.setCustomerNamePattern(customerTitle); |
||||
|
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, nodeConfiguration); |
||||
|
|
||||
|
var originator = toOriginator(type); |
||||
|
var msg = getTbMsg(originator); |
||||
|
|
||||
|
// we search for the customer only if incoming message originator is dashboard.
|
||||
|
if (type.equals(EntityType.DASHBOARD)) { |
||||
|
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock); |
||||
|
when(customerServiceMock.findCustomerByTenantIdAndTitle(eq(TENANT_ID), eq(customerTitle))).thenReturn(Optional.empty()); |
||||
|
|
||||
|
// DASHBOARD WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// DASHBOARD THEN
|
||||
|
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class); |
||||
|
verify(ctxMock).tellFailure(eq(msg), throwableCaptor.capture()); |
||||
|
assertThat(throwableCaptor.getValue()).hasMessage("Customer with title '" + customerTitle + "' doesn't exist!"); |
||||
|
|
||||
|
verifyNoMoreInteractions(customerServiceMock); |
||||
|
verifyNoMoreInteractions(ctxMock); |
||||
|
return; |
||||
|
} |
||||
|
Map<EntityType, Consumer<EntityId>> entityTypeToEntityIdConsumerMap = mockMethodCallsForSupportedTypes(); |
||||
|
entityTypeToEntityIdConsumerMap.get(type).accept(originator); |
||||
|
|
||||
|
// OTHER TYPES WHEN
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
// OTHER TYPES THEN
|
||||
|
verifyMsgSuccess(msg); |
||||
|
verifyNoMoreInteractions(ctxMock); |
||||
|
} |
||||
|
|
||||
|
private Map<EntityType, Consumer<EntityId>> mockMethodCallsForSupportedTypes() { |
||||
|
return Map.of( |
||||
|
EntityType.DEVICE, id -> { |
||||
|
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock); |
||||
|
when(deviceServiceMock.unassignDeviceFromCustomer(eq(TENANT_ID), (DeviceId) eq(id))) |
||||
|
.thenReturn(DEVICE); |
||||
|
}, |
||||
|
EntityType.ASSET, id -> { |
||||
|
when(ctxMock.getAssetService()).thenReturn(assetServiceMock); |
||||
|
when(assetServiceMock.unassignAssetFromCustomer(eq(TENANT_ID), (AssetId) eq(id))) |
||||
|
.thenReturn(ASSET); |
||||
|
}, |
||||
|
EntityType.ENTITY_VIEW, id -> { |
||||
|
when(ctxMock.getEntityViewService()).thenReturn(entityViewServiceMock); |
||||
|
when(entityViewServiceMock.unassignEntityViewFromCustomer(eq(TENANT_ID), (EntityViewId) eq(id))) |
||||
|
.thenReturn(ENTITY_VIEW); |
||||
|
}, |
||||
|
EntityType.EDGE, id -> { |
||||
|
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock); |
||||
|
when(edgeServiceMock.unassignEdgeFromCustomer(eq(TENANT_ID), (EdgeId) eq(id))) |
||||
|
.thenReturn(EDGE); |
||||
|
}, |
||||
|
EntityType.DASHBOARD, id -> { |
||||
|
when(ctxMock.getDashboardService()).thenReturn(dashboardServiceMock); |
||||
|
when(dashboardServiceMock.unassignDashboardFromCustomer(eq(TENANT_ID), (DashboardId) eq(id), any())) |
||||
|
.thenReturn(DASHBOARD); |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private void verifyMsgSuccess(TbMsg expectedMsg) { |
||||
|
verify(ctxMock).tellSuccess(eq(expectedMsg)); |
||||
|
verify(ctxMock, never()).tellFailure(any(), any()); |
||||
|
} |
||||
|
|
||||
|
private Customer createCustomer(String customerTitle) { |
||||
|
var customer = new Customer(); |
||||
|
customer.setTitle(customerTitle); |
||||
|
customer.setId(new CustomerId(UUID.randomUUID())); |
||||
|
customer.setTenantId(TENANT_ID); |
||||
|
return customer; |
||||
|
} |
||||
|
|
||||
|
private TbMsg getTbMsg(EntityId originator) { |
||||
|
return TbMsg.newMsg(TbMsgType.NA, originator, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
} |
||||
|
|
||||
|
private static EntityId toOriginator(EntityType type) { |
||||
|
return EntityIdFactory.getByTypeAndId(type.name(), UUID.randomUUID().toString()); |
||||
|
} |
||||
|
|
||||
|
// Rule nodes upgrade
|
||||
|
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyHasChangesAndConfig() { |
||||
|
return Stream.of( |
||||
|
// default config for version 0
|
||||
|
Arguments.of(0, |
||||
|
"{\"customerNamePattern\":\"\",\"customerCacheExpiration\":300}", |
||||
|
true, |
||||
|
"{\"customerNamePattern\":\"\"}"), |
||||
|
// default config for version 1 with upgrade from version 0
|
||||
|
Arguments.of(0, |
||||
|
"{\"customerNamePattern\":\"\"}", |
||||
|
false, |
||||
|
"{\"customerNamePattern\":\"\"}") |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue