10 changed files with 656 additions and 80 deletions
@ -0,0 +1,260 @@ |
|||
/** |
|||
* 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.CalculatedFieldId; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.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 java.util.Collections; |
|||
import java.util.List; |
|||
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.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
public class DefaultCalculatedFieldCacheTest { |
|||
|
|||
@Mock |
|||
private CalculatedFieldService calculatedFieldService; |
|||
@Mock |
|||
private DeviceService deviceService; |
|||
@Mock |
|||
private AssetService assetService; |
|||
@Mock |
|||
private CustomerService customerService; |
|||
|
|||
private DefaultCalculatedFieldCache cache; |
|||
|
|||
@BeforeEach |
|||
public void setUp() { |
|||
cache = new DefaultCalculatedFieldCache(calculatedFieldService, null, null); |
|||
} |
|||
|
|||
// --- 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_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_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(); |
|||
} |
|||
|
|||
// --- 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); |
|||
} |
|||
|
|||
// --- 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.emptyList()); |
|||
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(List.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,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.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(); |
|||
// tenant2 profile should still be served from cache (no extra service call)
|
|||
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