22 changed files with 1116 additions and 66 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