committed by
GitHub
14 changed files with 1016 additions and 52 deletions
@ -0,0 +1,517 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.cf; |
|||
|
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.cf.CalculatedField; |
|||
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; |
|||
import org.thingsboard.server.common.data.cf.CalculatedFieldType; |
|||
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; |
|||
import org.thingsboard.server.common.data.id.AssetId; |
|||
import org.thingsboard.server.common.data.id.AssetProfileId; |
|||
import org.thingsboard.server.common.data.id.CalculatedFieldId; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.DeviceProfileId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.TenantProfileId; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
|||
import org.thingsboard.server.dao.asset.AssetService; |
|||
import org.thingsboard.server.dao.cf.CalculatedFieldService; |
|||
import org.thingsboard.server.dao.customer.CustomerService; |
|||
import org.thingsboard.server.dao.device.DeviceService; |
|||
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; |
|||
import org.thingsboard.server.service.profile.TbAssetProfileCache; |
|||
import org.thingsboard.server.service.profile.TbDeviceProfileCache; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.never; |
|||
import static org.mockito.Mockito.spy; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class DefaultCalculatedFieldCacheTest { |
|||
|
|||
@Mock |
|||
private CalculatedFieldService calculatedFieldService; |
|||
@Mock |
|||
private TbAssetProfileCache assetProfileCache; |
|||
@Mock |
|||
private TbDeviceProfileCache deviceProfileCache; |
|||
@Mock |
|||
private TbTenantProfileCache tenantProfileCache; |
|||
@Mock |
|||
private DeviceService deviceService; |
|||
@Mock |
|||
private AssetService assetService; |
|||
@Mock |
|||
private CustomerService customerService; |
|||
|
|||
private DefaultCalculatedFieldCache cache; |
|||
|
|||
@BeforeEach |
|||
public void setUp() { |
|||
// ActorSystemContext is only used in getCalculatedFieldCtx (not tested here), so null is safe
|
|||
OwnerService ownerService = new OwnerService(deviceService, assetService, customerService); |
|||
cache = new DefaultCalculatedFieldCache(calculatedFieldService, assetProfileCache, |
|||
deviceProfileCache, tenantProfileCache, null, ownerService); |
|||
|
|||
} |
|||
|
|||
// --- Tenant deletion tests ---
|
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantDeleted_evictsAllTenantCfsFromAllMaps() { |
|||
TenantId tenant1 = new TenantId(UUID.randomUUID()); |
|||
TenantId tenant2 = new TenantId(UUID.randomUUID()); |
|||
DeviceId device1 = new DeviceId(UUID.randomUUID()); |
|||
DeviceId device2 = new DeviceId(UUID.randomUUID()); |
|||
|
|||
CalculatedField cf1 = addCfToCache(tenant1, device1); |
|||
CalculatedField cf2 = addCfToCache(tenant2, device2); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf1.getId())).isNull(); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(device1)).isEmpty(); |
|||
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(device2)).containsExactly(cf2); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantDeleted_evictsOwnerEntities() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
stubDeviceOwner(tenant, device, tenant); |
|||
|
|||
cache.addOwnerEntity(tenant, device); |
|||
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(device); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
// After eviction, getDynamicEntities triggers a fresh load from ownerService (empty)
|
|||
assertThat(cache.getDynamicEntities(tenant, tenant)).doesNotContain(device); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantDeleted_removesLinksToLinkedEntities() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceId cfEntity = new DeviceId(UUID.randomUUID()); |
|||
DeviceId linkedDevice = new DeviceId(UUID.randomUUID()); |
|||
|
|||
CalculatedField cf = addCfToCache(tenant, cfEntity, linkedDevice); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty(); |
|||
assertThat(cache.getCalculatedField(cf.getId())).isNull(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictCfs() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
CalculatedField cf = addCfToCache(tenant, device); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf); |
|||
} |
|||
|
|||
// --- Device/Asset deletion tests ---
|
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_deviceDeleted_evictsCfsForThatDevice() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
CalculatedField cf = addCfToCache(tenant, device); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf.getId())).isNull(); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(device)).isEmpty(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_deviceDeleted_removesLinksForLinkedEntities() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
DeviceId linkedDevice = new DeviceId(UUID.randomUUID()); |
|||
addCfToCache(tenant, device, linkedDevice); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_deviceDeleted_evictsDeviceFromOwnerEntities() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
CustomerId customer = new CustomerId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
stubDeviceOwner(tenant, device, customer); |
|||
|
|||
cache.addOwnerEntity(tenant, device); |
|||
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_assetDeleted_evictsCfsForThatAsset() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
AssetId asset = new AssetId(UUID.randomUUID()); |
|||
CalculatedField cf = addCfToCache(tenant, asset); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, asset, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf.getId())).isNull(); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(asset)).isEmpty(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_deviceCreated_addsDeviceToOwnerEntities() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
CustomerId customer = new CustomerId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
stubDeviceOwner(tenant, device, customer); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, device, ComponentLifecycleEvent.CREATED)); |
|||
|
|||
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device); |
|||
} |
|||
|
|||
// --- Customer deletion tests ---
|
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_customerDeleted_evictsCustomerOwnerEntries() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
CustomerId customer = new CustomerId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
stubDeviceOwner(tenant, device, customer); |
|||
|
|||
cache.addOwnerEntity(tenant, device); |
|||
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, customer, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
// The customer's owned-entities entry is evicted; fresh load returns empty
|
|||
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device); |
|||
} |
|||
|
|||
// --- DeviceProfile/AssetProfile deletion tests ---
|
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_deviceProfileDeleted_evictsCfsForThatProfile() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID()); |
|||
CalculatedField cf = addCfToCache(tenant, profileId); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf.getId())).isNull(); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).isEmpty(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_deviceProfileDeleted_removesLinksForLinkedEntities() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID()); |
|||
DeviceId linkedDevice = new DeviceId(UUID.randomUUID()); |
|||
addCfToCache(tenant, profileId, linkedDevice); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedDevice)).isEmpty(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_deviceProfileDeleted_doesNotEvictOtherProfilesCfs() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceProfileId profile1 = new DeviceProfileId(UUID.randomUUID()); |
|||
DeviceProfileId profile2 = new DeviceProfileId(UUID.randomUUID()); |
|||
CalculatedField cf1 = addCfToCache(tenant, profile1); |
|||
CalculatedField cf2 = addCfToCache(tenant, profile2); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profile1, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf1.getId())).isNull(); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(profile1)).isEmpty(); |
|||
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(profile2)).containsExactly(cf2); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_deviceProfileUpdated_doesNotEvictCfs() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID()); |
|||
CalculatedField cf = addCfToCache(tenant, profileId); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).containsExactly(cf); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_assetProfileDeleted_evictsCfsForThatProfile() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID()); |
|||
CalculatedField cf = addCfToCache(tenant, profileId); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf.getId())).isNull(); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).isEmpty(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_assetProfileDeleted_removesLinksForLinkedEntities() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID()); |
|||
AssetId linkedAsset = new AssetId(UUID.randomUUID()); |
|||
addCfToCache(tenant, profileId, linkedAsset); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedFieldLinksByEntityId(linkedAsset)).isEmpty(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_assetProfileDeleted_doesNotEvictOtherProfilesCfs() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
AssetProfileId profile1 = new AssetProfileId(UUID.randomUUID()); |
|||
AssetProfileId profile2 = new AssetProfileId(UUID.randomUUID()); |
|||
CalculatedField cf1 = addCfToCache(tenant, profile1); |
|||
CalculatedField cf2 = addCfToCache(tenant, profile2); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profile1, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf1.getId())).isNull(); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(profile1)).isEmpty(); |
|||
assertThat(cache.getCalculatedField(cf2.getId())).isEqualTo(cf2); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(profile2)).containsExactly(cf2); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_assetProfileUpdated_doesNotEvictCfs() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID()); |
|||
CalculatedField cf = addCfToCache(tenant, profileId); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(cf); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(profileId)).containsExactly(cf); |
|||
} |
|||
|
|||
// --- CalculatedField lifecycle tests ---
|
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_calculatedFieldCreated_addsCfToCache() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID()); |
|||
CalculatedField cf = buildCalculatedField(cfId, tenant, device, simpleCfConfig()); |
|||
when(calculatedFieldService.findById(tenant, cfId)).thenReturn(cf); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cfId, ComponentLifecycleEvent.CREATED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cfId)).isEqualTo(cf); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(device)).containsExactly(cf); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_calculatedFieldDeleted_evictsCfFromCache() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
CalculatedField cf = addCfToCache(tenant, device); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cf.getId(), ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf.getId())).isNull(); |
|||
assertThat(cache.getCalculatedFieldsByEntityId(device)).isEmpty(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_calculatedFieldUpdated_refreshesCfInCache() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
CalculatedField cf = addCfToCache(tenant, device); |
|||
|
|||
CalculatedField updatedCf = buildCalculatedField(cf.getId(), tenant, device, simpleCfConfig()); |
|||
updatedCf.setName("updated-name"); |
|||
when(calculatedFieldService.findById(tenant, cf.getId())).thenReturn(updatedCf); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, cf.getId(), ComponentLifecycleEvent.UPDATED)); |
|||
|
|||
assertThat(cache.getCalculatedField(cf.getId())).isEqualTo(updatedCf); |
|||
} |
|||
|
|||
// --- evictOwner recursive traversal tests ---
|
|||
|
|||
@Test |
|||
public void evictOwner_customerDeleted_recursivelyEvictsDevicesOwnedByThatCustomer() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
CustomerId customer = new CustomerId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
|
|||
stubDeviceOwner(tenant, device, customer); |
|||
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData()); |
|||
|
|||
// tenant owns customer (getOwner for CUSTOMER returns tenantId)
|
|||
cache.addOwnerEntity(tenant, customer); // ownerEntities[tenant] = {customer}
|
|||
cache.addOwnerEntity(tenant, device); // ownerEntities[customer] = {device}
|
|||
|
|||
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(customer); |
|||
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device); |
|||
|
|||
// deleting the customer evicts the customer key and recursively cleans its owned set
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, customer, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device); |
|||
} |
|||
|
|||
@Test |
|||
public void evictOwner_tenantDeleted_recursivelyEvictsCustomerAndItsOwnedDevices() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
CustomerId customer = new CustomerId(UUID.randomUUID()); |
|||
DeviceId device = new DeviceId(UUID.randomUUID()); |
|||
|
|||
stubDeviceOwner(tenant, device, customer); |
|||
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData()); |
|||
|
|||
cache.addOwnerEntity(tenant, customer); // ownerEntities[tenant] = {customer}
|
|||
cache.addOwnerEntity(tenant, device); // ownerEntities[customer] = {device}
|
|||
|
|||
assertThat(cache.getDynamicEntities(tenant, tenant)).contains(customer); |
|||
assertThat(cache.getDynamicEntities(tenant, customer)).contains(device); |
|||
|
|||
// deleting the tenant: evictOwner(tenant) finds customer (CUSTOMER type) and recurses into it
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
// both levels must be gone
|
|||
assertThat(cache.getDynamicEntities(tenant, tenant)).doesNotContain(customer); |
|||
assertThat(cache.getDynamicEntities(tenant, customer)).doesNotContain(device); |
|||
} |
|||
|
|||
// --- TenantProfile lifecycle tests ---
|
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantProfileUpdated_callsHandleTenantProfileUpdate() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
TenantProfileId profileId = new TenantProfileId(UUID.randomUUID()); |
|||
DefaultCalculatedFieldCache spyCache = spy(cache); |
|||
|
|||
spyCache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.UPDATED)); |
|||
|
|||
verify(spyCache).handleTenantProfileUpdate(profileId); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantProfileDeleted_doesNotCallHandleTenantProfileUpdate() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
TenantProfileId profileId = new TenantProfileId(UUID.randomUUID()); |
|||
DefaultCalculatedFieldCache spyCache = spy(cache); |
|||
|
|||
spyCache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, profileId, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
verify(spyCache, never()).handleTenantProfileUpdate(any()); |
|||
} |
|||
|
|||
// --- Helpers ---
|
|||
|
|||
private void stubDeviceOwner(TenantId tenantId, DeviceId deviceId, EntityId ownerId) { |
|||
Device device = new Device(); |
|||
device.setId(deviceId); |
|||
device.setTenantId(tenantId); |
|||
if (ownerId instanceof CustomerId customerId) { |
|||
device.setCustomerId(customerId); |
|||
} |
|||
// If ownerId is a TenantId, leaving customerId null means getOwnerId() returns tenantId
|
|||
when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device); |
|||
// Stubs for getOwnedEntities iteration (empty pages — device is added explicitly)
|
|||
when(deviceService.findDeviceInfosByFilter(any(), any())).thenReturn(PageData.emptyPageData()); |
|||
when(assetService.findAssetsByTenantIdAndCustomerId(any(), any(), any())).thenReturn(PageData.emptyPageData()); |
|||
if (ownerId instanceof TenantId) { |
|||
when(customerService.findCustomersByTenantId(any(), any())).thenReturn(PageData.emptyPageData()); |
|||
} |
|||
} |
|||
|
|||
private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId) { |
|||
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID()); |
|||
CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, simpleCfConfig()); |
|||
when(calculatedFieldService.findById(tenantId, cfId)).thenReturn(cf); |
|||
cache.addCalculatedField(tenantId, cfId); |
|||
return cf; |
|||
} |
|||
|
|||
private CalculatedField addCfToCache(TenantId tenantId, EntityId entityId, EntityId linkedEntity) { |
|||
CalculatedFieldId cfId = new CalculatedFieldId(UUID.randomUUID()); |
|||
CalculatedFieldConfiguration config = linkedEntityCfConfig(tenantId, cfId, linkedEntity); |
|||
CalculatedField cf = buildCalculatedField(cfId, tenantId, entityId, config); |
|||
when(calculatedFieldService.findById(tenantId, cfId)).thenReturn(cf); |
|||
cache.addCalculatedField(tenantId, cfId); |
|||
return cf; |
|||
} |
|||
|
|||
private CalculatedField buildCalculatedField(CalculatedFieldId id, TenantId tenantId, EntityId entityId, CalculatedFieldConfiguration config) { |
|||
CalculatedField cf = new CalculatedField(); |
|||
cf.setId(id); |
|||
cf.setTenantId(tenantId); |
|||
cf.setEntityId(entityId); |
|||
cf.setType(CalculatedFieldType.SIMPLE); |
|||
cf.setName("test-cf-" + id.getId()); |
|||
cf.setConfiguration(config); |
|||
return cf; |
|||
} |
|||
|
|||
private CalculatedFieldConfiguration simpleCfConfig() { |
|||
CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class); |
|||
when(config.getReferencedEntities()).thenReturn(Collections.emptySet()); |
|||
when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(Collections.emptyList()); |
|||
return config; |
|||
} |
|||
|
|||
private CalculatedFieldConfiguration linkedEntityCfConfig(TenantId tenantId, CalculatedFieldId cfId, EntityId linkedEntity) { |
|||
CalculatedFieldConfiguration config = mock(CalculatedFieldConfiguration.class); |
|||
CalculatedFieldLink link = new CalculatedFieldLink(tenantId, linkedEntity, cfId); |
|||
when(config.getReferencedEntities()).thenReturn(Set.of(linkedEntity)); |
|||
when(config.buildCalculatedFieldLinks(any(), any(), any())).thenReturn(List.of(link)); |
|||
when(config.buildCalculatedFieldLink(any(), eq(linkedEntity), any())).thenReturn(link); |
|||
return config; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.profile; |
|||
|
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.thingsboard.server.common.data.asset.Asset; |
|||
import org.thingsboard.server.common.data.asset.AssetProfile; |
|||
import org.thingsboard.server.common.data.id.AssetId; |
|||
import org.thingsboard.server.common.data.id.AssetProfileId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
|||
import org.thingsboard.server.dao.asset.AssetProfileService; |
|||
import org.thingsboard.server.dao.asset.AssetService; |
|||
|
|||
import java.util.UUID; |
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.Mockito.times; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class DefaultTbAssetProfileCacheTest { |
|||
|
|||
@Mock |
|||
private AssetProfileService assetProfileService; |
|||
@Mock |
|||
private AssetService assetService; |
|||
|
|||
private DefaultTbAssetProfileCache cache; |
|||
|
|||
@BeforeEach |
|||
public void setUp() { |
|||
cache = new DefaultTbAssetProfileCache(assetProfileService, assetService); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantDeleted_evictsAssetProfilesForThatTenant() { |
|||
TenantId tenant1 = new TenantId(UUID.randomUUID()); |
|||
TenantId tenant2 = new TenantId(UUID.randomUUID()); |
|||
AssetProfileId profileId1 = new AssetProfileId(UUID.randomUUID()); |
|||
AssetProfileId profileId2 = new AssetProfileId(UUID.randomUUID()); |
|||
|
|||
loadProfileIntoCache(tenant1, profileId1); |
|||
loadProfileIntoCache(tenant2, profileId2); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
// After deletion tenant1 profile should be reloaded from service on next get
|
|||
when(assetProfileService.findAssetProfileById(any(), any())).thenReturn(null); |
|||
assertThat(cache.get(tenant1, profileId1)).isNull(); |
|||
verify(assetProfileService, times(1)).findAssetProfileById(tenant2, profileId2); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantDeleted_evictsAssetMappingsForThatTenant() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID()); |
|||
AssetId assetId = new AssetId(UUID.randomUUID()); |
|||
|
|||
loadProfileIntoCache(tenant, profileId); |
|||
loadAssetMappingIntoCache(tenant, assetId, profileId); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
// After tenant deletion, asset-to-profile mapping should be gone; get() should try to reload
|
|||
when(assetService.findAssetById(any(), any())).thenReturn(null); |
|||
assertThat(cache.get(tenant, assetId)).isNull(); |
|||
verify(assetService, times(2)).findAssetById(tenant, assetId); // once on load, once after eviction
|
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantDeleted_removesListenersForThatTenant() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
EntityId listenerId = new AssetId(UUID.randomUUID()); |
|||
AtomicInteger callCount = new AtomicInteger(); |
|||
|
|||
cache.addListener(tenant, listenerId, profile -> callCount.incrementAndGet(), null); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
// Evicting a profile after tenant deletion should not trigger the removed listener
|
|||
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID()); |
|||
loadProfileIntoCache(tenant, profileId); |
|||
cache.evict(tenant, profileId); |
|||
|
|||
assertThat(callCount.get()).isZero(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictProfiles() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
AssetProfileId profileId = new AssetProfileId(UUID.randomUUID()); |
|||
loadProfileIntoCache(tenant, profileId); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED)); |
|||
|
|||
// Profile should still be served from cache without hitting the service again
|
|||
cache.get(tenant, profileId); |
|||
verify(assetProfileService, times(1)).findAssetProfileById(tenant, profileId); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_differentTenantDeleted_keepsOtherTenantsProfiles() { |
|||
TenantId tenant1 = new TenantId(UUID.randomUUID()); |
|||
TenantId tenant2 = new TenantId(UUID.randomUUID()); |
|||
AssetProfileId profileId1 = new AssetProfileId(UUID.randomUUID()); |
|||
AssetProfileId profileId2 = new AssetProfileId(UUID.randomUUID()); |
|||
|
|||
AssetProfile profile1 = loadProfileIntoCache(tenant1, profileId1); |
|||
loadProfileIntoCache(tenant2, profileId2); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant2, tenant2, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.get(tenant1, profileId1)).isEqualTo(profile1); |
|||
verify(assetProfileService, times(1)).findAssetProfileById(tenant1, profileId1); |
|||
} |
|||
|
|||
// --- Helpers ---
|
|||
|
|||
private AssetProfile loadProfileIntoCache(TenantId tenantId, AssetProfileId profileId) { |
|||
AssetProfile profile = new AssetProfile(); |
|||
profile.setId(profileId); |
|||
profile.setTenantId(tenantId); |
|||
when(assetProfileService.findAssetProfileById(tenantId, profileId)).thenReturn(profile); |
|||
cache.get(tenantId, profileId); |
|||
return profile; |
|||
} |
|||
|
|||
private void loadAssetMappingIntoCache(TenantId tenantId, AssetId assetId, AssetProfileId profileId) { |
|||
Asset asset = new Asset(); |
|||
asset.setId(assetId); |
|||
asset.setAssetProfileId(profileId); |
|||
when(assetService.findAssetById(tenantId, assetId)).thenReturn(asset); |
|||
cache.get(tenantId, assetId); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
/** |
|||
* Copyright © 2016-2026 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.profile; |
|||
|
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.DeviceProfileId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; |
|||
import org.thingsboard.server.dao.device.DeviceProfileService; |
|||
import org.thingsboard.server.dao.device.DeviceService; |
|||
|
|||
import java.util.UUID; |
|||
import java.util.concurrent.atomic.AtomicInteger; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.Mockito.times; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class DefaultTbDeviceProfileCacheTest { |
|||
|
|||
@Mock |
|||
private DeviceProfileService deviceProfileService; |
|||
@Mock |
|||
private DeviceService deviceService; |
|||
|
|||
private DefaultTbDeviceProfileCache cache; |
|||
|
|||
@BeforeEach |
|||
public void setUp() { |
|||
cache = new DefaultTbDeviceProfileCache(deviceProfileService, deviceService); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantDeleted_evictsDeviceProfilesForThatTenant() { |
|||
TenantId tenant1 = new TenantId(UUID.randomUUID()); |
|||
TenantId tenant2 = new TenantId(UUID.randomUUID()); |
|||
DeviceProfileId profileId1 = new DeviceProfileId(UUID.randomUUID()); |
|||
DeviceProfileId profileId2 = new DeviceProfileId(UUID.randomUUID()); |
|||
|
|||
loadProfileIntoCache(tenant1, profileId1); |
|||
loadProfileIntoCache(tenant2, profileId2); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant1, tenant1, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
// After deletion tenant1 profile should be reloaded from service on next get
|
|||
when(deviceProfileService.findDeviceProfileById(any(), any())).thenReturn(null); |
|||
assertThat(cache.get(tenant1, profileId1)).isNull(); |
|||
// tenant2 profile should still be served from cache (no extra service call)
|
|||
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant2, profileId2); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantDeleted_evictsDeviceMappingsForThatTenant() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID()); |
|||
DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
|||
|
|||
loadProfileIntoCache(tenant, profileId); |
|||
loadDeviceMappingIntoCache(tenant, deviceId, profileId); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
// After tenant deletion, device-to-profile mapping should be gone; get() should try to reload
|
|||
when(deviceService.findDeviceById(any(), any())).thenReturn(null); |
|||
assertThat(cache.get(tenant, deviceId)).isNull(); |
|||
verify(deviceService, times(2)).findDeviceById(tenant, deviceId); // once on load, once after eviction
|
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantDeleted_removesListenersForThatTenant() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
EntityId listenerId = new DeviceId(UUID.randomUUID()); |
|||
AtomicInteger callCount = new AtomicInteger(); |
|||
|
|||
cache.addListener(tenant, listenerId, profile -> callCount.incrementAndGet(), null); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
// Evicting a profile after tenant deletion should not trigger the removed listener
|
|||
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID()); |
|||
loadProfileIntoCache(tenant, profileId); |
|||
cache.evict(tenant, profileId); |
|||
|
|||
assertThat(callCount.get()).isZero(); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_tenantUpdated_doesNotEvictProfiles() { |
|||
TenantId tenant = new TenantId(UUID.randomUUID()); |
|||
DeviceProfileId profileId = new DeviceProfileId(UUID.randomUUID()); |
|||
loadProfileIntoCache(tenant, profileId); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant, tenant, ComponentLifecycleEvent.UPDATED)); |
|||
|
|||
// Profile should still be served from cache without hitting the service again
|
|||
cache.get(tenant, profileId); |
|||
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant, profileId); |
|||
} |
|||
|
|||
@Test |
|||
public void onComponentLifecycleEvent_differentTenantDeleted_keepsOtherTenantsProfiles() { |
|||
TenantId tenant1 = new TenantId(UUID.randomUUID()); |
|||
TenantId tenant2 = new TenantId(UUID.randomUUID()); |
|||
DeviceProfileId profileId1 = new DeviceProfileId(UUID.randomUUID()); |
|||
DeviceProfileId profileId2 = new DeviceProfileId(UUID.randomUUID()); |
|||
|
|||
DeviceProfile profile1 = loadProfileIntoCache(tenant1, profileId1); |
|||
loadProfileIntoCache(tenant2, profileId2); |
|||
|
|||
cache.onComponentLifecycleEvent(new ComponentLifecycleMsg(tenant2, tenant2, ComponentLifecycleEvent.DELETED)); |
|||
|
|||
assertThat(cache.get(tenant1, profileId1)).isEqualTo(profile1); |
|||
verify(deviceProfileService, times(1)).findDeviceProfileById(tenant1, profileId1); |
|||
} |
|||
|
|||
// --- Helpers ---
|
|||
|
|||
private DeviceProfile loadProfileIntoCache(TenantId tenantId, DeviceProfileId profileId) { |
|||
DeviceProfile profile = new DeviceProfile(); |
|||
profile.setId(profileId); |
|||
profile.setTenantId(tenantId); |
|||
when(deviceProfileService.findDeviceProfileById(tenantId, profileId)).thenReturn(profile); |
|||
cache.get(tenantId, profileId); |
|||
return profile; |
|||
} |
|||
|
|||
private void loadDeviceMappingIntoCache(TenantId tenantId, DeviceId deviceId, DeviceProfileId profileId) { |
|||
Device device = new Device(); |
|||
device.setId(deviceId); |
|||
device.setDeviceProfileId(profileId); |
|||
when(deviceService.findDeviceById(tenantId, deviceId)).thenReturn(device); |
|||
cache.get(tenantId, deviceId); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue