From ec8e7a07268694685cfb047f1fc7724a36e50b2d Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 10 Feb 2025 14:28:46 +0200 Subject: [PATCH 1/9] Added versioning for ApiUsageState entity --- .../src/main/data/upgrade/basic/schema_update.sql | 2 ++ .../thingsboard/server/common/data/ApiUsageState.java | 5 ++++- .../server/dao/model/sql/ApiUsageStateEntity.java | 10 ++++------ dao/src/main/resources/sql/schema-entities.sql | 1 + 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 5d66056701..29b541327a 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -61,3 +61,5 @@ DO $$ $$; -- UPDATE SAVE TIME SERIES NODES END + +ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1; \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java index b13a9edd74..f6f3d82c09 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.id.TenantId; @EqualsAndHashCode(callSuper = true) @Getter @Setter -public class ApiUsageState extends BaseData implements HasTenantId { +public class ApiUsageState extends BaseData implements HasTenantId, HasVersion { private static final long serialVersionUID = 8250339805336035966L; @@ -41,6 +41,7 @@ public class ApiUsageState extends BaseData implements HasTenan private ApiUsageStateValue emailExecState; private ApiUsageStateValue smsExecState; private ApiUsageStateValue alarmExecState; + private Long version; public ApiUsageState() { super(); @@ -62,6 +63,7 @@ public class ApiUsageState extends BaseData implements HasTenan this.emailExecState = ur.getEmailExecState(); this.smsExecState = ur.getSmsExecState(); this.alarmExecState = ur.getAlarmExecState(); + this.version = ur.getVersion(); } public boolean isTransportEnabled() { @@ -95,4 +97,5 @@ public class ApiUsageState extends BaseData implements HasTenan public boolean isAlarmCreationEnabled() { return alarmExecState != ApiUsageStateValue.DISABLED; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java index 43b99dd7ac..2a65029742 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ApiUsageStateEntity.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.id.ApiUsageStateId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseEntity; -import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.BaseVersionedEntity; import org.thingsboard.server.dao.model.ModelConstants; import java.util.UUID; @@ -40,7 +40,7 @@ import java.util.UUID; @EqualsAndHashCode(callSuper = true) @Entity @Table(name = ModelConstants.API_USAGE_STATE_TABLE_NAME) -public class ApiUsageStateEntity extends BaseSqlEntity implements BaseEntity { +public class ApiUsageStateEntity extends BaseVersionedEntity implements BaseEntity { @Column(name = ModelConstants.API_USAGE_STATE_TENANT_ID_COLUMN) private UUID tenantId; @@ -77,10 +77,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity implements } public ApiUsageStateEntity(ApiUsageState ur) { - if (ur.getId() != null) { - this.setUuid(ur.getId().getId()); - } - this.setCreatedTime(ur.getCreatedTime()); + super(ur); if (ur.getTenantId() != null) { this.tenantId = ur.getTenantId().getId(); } @@ -116,6 +113,7 @@ public class ApiUsageStateEntity extends BaseSqlEntity implements ur.setEmailExecState(emailExecState); ur.setSmsExecState(smsExecState); ur.setAlarmExecState(alarmExecState); + ur.setVersion(version); return ur; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 916a5487f8..71afc03081 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -702,6 +702,7 @@ CREATE TABLE IF NOT EXISTS api_usage_state ( email_exec varchar(32), sms_exec varchar(32), alarm_exec varchar(32), + version BIGINT DEFAULT 1, CONSTRAINT api_usage_state_unq_key UNIQUE (tenant_id, entity_id) ); From 61f77f301d2a08d2448b4d8517cbfb720ace32e7 Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Fri, 14 Feb 2025 19:21:03 +0200 Subject: [PATCH 2/9] Resolved review comments --- .../service/apiusage/BaseApiUsageState.java | 3 +- .../DefaultTbApiUsageStateService.java | 3 +- .../DefaultTbApiUsageStateServiceTest.java | 142 ++++++++++++++++++ .../dao/usagerecord/ApiUsageStateService.java | 1 + .../server/common/data/ApiUsageState.java | 4 +- .../server/common/util/ProtoUtils.java | 5 +- common/proto/src/main/proto/queue.proto | 1 + .../usagerecord/ApiUsageStateServiceImpl.java | 1 + .../dao/service/ApiUsageStateServiceTest.java | 47 ++++-- 9 files changed, 189 insertions(+), 18 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java index 9eac721dfb..9db4949d5f 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/BaseApiUsageState.java @@ -40,7 +40,8 @@ public abstract class BaseApiUsageState { private final Map gaugesReportCycles = new HashMap<>(); @Getter - private final ApiUsageState apiUsageState; + @Setter + private ApiUsageState apiUsageState; @Getter private volatile long currentCycleTs; @Getter diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index 3a351528e4..30a3af7f65 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -355,7 +355,8 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService private void persistAndNotify(BaseApiUsageState state, Map result) { log.info("[{}] Detected update of the API state for {}: {}", state.getEntityId(), state.getEntityType(), result); - apiUsageStateService.update(state.getApiUsageState()); + ApiUsageState updatedState = apiUsageStateService.update(state.getApiUsageState()); + state.setApiUsageState(updatedState); long ts = System.currentTimeMillis(); List stateTelemetry = new ArrayList<>(); result.forEach((apiFeature, aState) -> stateTelemetry.add(new BasicTsKvEntry(ts, new StringDataEntry(apiFeature.getApiStateKey(), aState.name())))); diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 051214c771..a0f61a4928 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.apiusage; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,14 +24,43 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.ApiFeature; +import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageState; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitTrigger; +import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.service.mail.MailExecutorService; +import org.thingsboard.server.service.telemetry.InternalTelemetryService; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class DefaultTbApiUsageStateServiceTest { @@ -38,6 +68,21 @@ public class DefaultTbApiUsageStateServiceTest { @Mock TenantApiUsageState tenantUsageStateMock; + @Mock + private NotificationRuleProcessor notificationRuleProcessor; + + @Mock + private ApiUsageStateService apiUsageStateService; + + @Mock + private TenantService tenantService; + + @Mock + private InternalTelemetryService tsWsService; + + @Mock + private MailExecutorService mailExecutor; + TenantId tenantId = TenantId.fromUUID(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); @Spy @@ -46,6 +91,7 @@ public class DefaultTbApiUsageStateServiceTest { @BeforeEach public void setUp() { + ReflectionTestUtils.setField(service, "tsWsService", tsWsService); } @Test @@ -56,4 +102,100 @@ public class DefaultTbApiUsageStateServiceTest { Mockito.verify(service, never()).getOrFetchState(tenantId, tenantId); } + @Test + public void testAllApiFeaturesDisabledWhenLimitReached() { + doReturn(null).when(tsWsService).saveTimeseriesInternal(any()); + + TenantApiUsageState tenantUsageStateMock = mock(TenantApiUsageState.class); + ApiUsageState apiUsageState = getApiUsageState(); + when(tenantUsageStateMock.getApiUsageState()).thenReturn(apiUsageState); + + doReturn(BaseApiUsageState.StatsCalculationResult.builder() + .newValue(200L) + .valueChanged(true) + .newHourlyValue(200L) + .hourlyValueChanged(true) + .build()) + .when(tenantUsageStateMock).calculate(any(ApiUsageRecordKey.class), anyLong(), anyString()); + + doReturn(200L).when(tenantUsageStateMock).getProfileThreshold(any(ApiUsageRecordKey.class)); + doReturn(50L).when(tenantUsageStateMock).getProfileWarnThreshold(any(ApiUsageRecordKey.class)); + doReturn(300L).when(tenantUsageStateMock).get(any(ApiUsageRecordKey.class)); + + when(tenantUsageStateMock.getEntityType()).thenReturn(EntityType.TENANT); + when(tenantUsageStateMock.getEntityId()).thenReturn(tenantId); + + Map expectedResult = getExpectedResult(); + when(tenantUsageStateMock.checkStateUpdatedDueToThreshold(any())).thenReturn(expectedResult); + service.myUsageStates.put(tenantId, tenantUsageStateMock); + + when(apiUsageStateService.update(apiUsageState)).thenReturn(apiUsageState); + + Tenant dummyTenant = new Tenant(); + dummyTenant.setEmail("test@example.com"); + when(tenantService.findTenantById(any())).thenReturn(dummyTenant); + + TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder(); + UUID uuid = tenantId.getId(); + msgBuilder.setTenantIdMSB(uuid.getMostSignificantBits()) + .setTenantIdLSB(uuid.getLeastSignificantBits()); + msgBuilder.setCustomerIdMSB(0) + .setCustomerIdLSB(0); + msgBuilder.setServiceId("TEST_SERVICE"); + + List usageStats = new ArrayList<>(); + for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.values()) { + if (recordKey.getApiFeature() != null) { + TransportProtos.UsageStatsKVProto stat = TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(recordKey.name()) + .setValue(1000L) + .build(); + usageStats.add(stat); + msgBuilder.addValues(stat); + } + } + + TransportProtos.ToUsageStatsServiceMsg statsMsg = msgBuilder.build(); + TbProtoQueueMsg queueMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); + TbCallback callback = mock(TbCallback.class); + + service.process(queueMsg, callback); + verify(callback).onSuccess(); + + for (ApiFeature feature : expectedResult.keySet()) { + verify(notificationRuleProcessor, atLeastOnce()).process(argThat(trigger -> + trigger instanceof ApiUsageLimitTrigger && + ((ApiUsageLimitTrigger) trigger).getStatus() == ApiUsageStateValue.DISABLED && + ((ApiUsageLimitTrigger) trigger).getState().getApiFeature().getApiStateKey().equals(feature.getApiStateKey()) + )); + } + } + + + @NotNull + private ApiUsageState getApiUsageState() { + ApiUsageState apiUsageState = new ApiUsageState(); + + apiUsageState.setTenantId(tenantId); + apiUsageState.setTransportState(ApiUsageStateValue.ENABLED); + apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); + apiUsageState.setReExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); + return apiUsageState; + } + + private Map getExpectedResult() { + Map expectedResult = new HashMap<>(); + for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.values()) { + if (recordKey.getApiFeature() != null) { + expectedResult.put(recordKey.getApiFeature(), ApiUsageStateValue.DISABLED); + } + } + + return expectedResult; + } } \ No newline at end of file diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java index 3d09247c7e..9512b72ecd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateService.java @@ -34,4 +34,5 @@ public interface ApiUsageStateService extends EntityDaoService { void deleteApiUsageStateByEntityId(EntityId entityId); ApiUsageState findApiUsageStateById(TenantId tenantId, ApiUsageStateId id); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java index f6f3d82c09..9b2a2b0046 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ApiUsageState.java @@ -86,11 +86,11 @@ public class ApiUsageState extends BaseData implements HasTenan return !ApiUsageStateValue.DISABLED.equals(tbelExecState); } - public boolean isEmailSendEnabled(){ + public boolean isEmailSendEnabled() { return !ApiUsageStateValue.DISABLED.equals(emailExecState); } - public boolean isSmsSendEnabled(){ + public boolean isSmsSendEnabled() { return !ApiUsageStateValue.DISABLED.equals(smsExecState); } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 264b23f118..dd376fe746 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -1019,7 +1019,9 @@ public class ProtoUtils { .setTbelExecState(apiUsageState.getTbelExecState().name()) .setEmailExecState(apiUsageState.getEmailExecState().name()) .setSmsExecState(apiUsageState.getSmsExecState().name()) - .setAlarmExecState(apiUsageState.getAlarmExecState().name()).build(); + .setAlarmExecState(apiUsageState.getAlarmExecState().name()) + .setVersion(apiUsageState.getVersion()) + .build(); } public static ApiUsageState fromProto(TransportProtos.ApiUsageStateProto proto) { @@ -1035,6 +1037,7 @@ public class ProtoUtils { apiUsageState.setEmailExecState(ApiUsageStateValue.valueOf(proto.getEmailExecState())); apiUsageState.setSmsExecState(ApiUsageStateValue.valueOf(proto.getSmsExecState())); apiUsageState.setAlarmExecState(ApiUsageStateValue.valueOf(proto.getAlarmExecState())); + apiUsageState.setVersion(proto.getVersion()); return apiUsageState; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 228a4039d2..25c1f5dd01 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -325,6 +325,7 @@ message ApiUsageStateProto { string emailExecState = 14; string smsExecState = 15; string alarmExecState = 16; + int64 version = 17; } message RepositorySettingsProto { diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java index d901ae950e..3d0e0f1309 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/ApiUsageStateServiceImpl.java @@ -154,6 +154,7 @@ public class ApiUsageStateServiceImpl extends AbstractEntityService implements A log.trace("Executing save [{}]", apiUsageState.getTenantId()); validateId(apiUsageState.getTenantId(), id -> INCORRECT_TENANT_ID + id); validateId(apiUsageState.getId(), "Can't save new usage state. Only update is allowed!"); + apiUsageState.setVersion(null); ApiUsageState savedState = apiUsageStateDao.save(apiUsageState.getTenantId(), apiUsageState); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedState.getTenantId()).entityId(savedState.getId()) .entity(savedState).build()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java index 36b138f42a..d3e86a1603 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/ApiUsageStateServiceTest.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; @@ -30,22 +31,42 @@ public class ApiUsageStateServiceTest extends AbstractServiceTest { ApiUsageStateService apiUsageStateService; @Test - public void testFindApiUsageStateByTenantId() { - ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId); - Assert.assertNotNull(apiUsageState); + public void testFindTenantApiUsageState() { + ApiUsageState state = apiUsageStateService.findTenantApiUsageState(tenantId); + Assert.assertNotNull(state); } @Test - public void testUpdateApiUsageState(){ - ApiUsageState apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId); - Assert.assertNotNull(apiUsageState); - Assert.assertTrue(apiUsageState.isTransportEnabled()); - apiUsageState.setTransportState(ApiUsageStateValue.DISABLED); - apiUsageState = apiUsageStateService.update(apiUsageState); - Assert.assertNotNull(apiUsageState); - apiUsageState = apiUsageStateService.findTenantApiUsageState(tenantId); - Assert.assertNotNull(apiUsageState); - Assert.assertFalse(apiUsageState.isTransportEnabled()); + public void testUpdate() { + ApiUsageState state = apiUsageStateService.findTenantApiUsageState(tenantId); + + state.setTransportState(ApiUsageStateValue.DISABLED); + ApiUsageState updated = apiUsageStateService.update(state); + Assert.assertEquals(ApiUsageStateValue.DISABLED, updated.getTransportState()); + } + + @Test + public void testUpdateWithNullId() { + ApiUsageState newState = new ApiUsageState(); + newState.setTenantId(tenantId); + newState.setTransportState(ApiUsageStateValue.ENABLED); + Assert.assertThrows(IncorrectParameterException.class, () -> apiUsageStateService.update(newState)); + } + + @Test + public void testFindApiUsageStateByEntityId() { + ApiUsageState state = apiUsageStateService.findApiUsageStateByEntityId(tenantId); + Assert.assertNotNull(state); + } + + @Test + public void testDeleteByTenantId() { + ApiUsageState state = apiUsageStateService.findTenantApiUsageState(tenantId); + Assert.assertNotNull(state); + + apiUsageStateService.deleteByTenantId(tenantId); + state = apiUsageStateService.findTenantApiUsageState(tenantId); + Assert.assertNull(state); } } From 9542c65ca3d00f32d161fdd09ce162d1b50ee024 Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 17 Feb 2025 15:39:59 +0200 Subject: [PATCH 3/9] Fixed api limit tests for ApiUsage service --- .../DefaultTbApiUsageStateServiceTest.java | 260 +++++++++++------- 1 file changed, 153 insertions(+), 107 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index a0f61a4928..100c98adec 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -15,7 +15,8 @@ */ package org.thingsboard.server.service.apiusage; -import org.jetbrains.annotations.NotNull; +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -25,42 +26,28 @@ import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; -import org.thingsboard.server.common.data.ApiFeature; -import org.thingsboard.server.common.data.ApiUsageRecordKey; -import org.thingsboard.server.common.data.ApiUsageState; -import org.thingsboard.server.common.data.ApiUsageStateValue; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitTrigger; +import org.thingsboard.server.common.data.id.TenantProfileId; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.service.mail.MailExecutorService; import org.thingsboard.server.service.telemetry.InternalTelemetryService; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class DefaultTbApiUsageStateServiceTest { @@ -80,122 +67,181 @@ public class DefaultTbApiUsageStateServiceTest { @Mock private InternalTelemetryService tsWsService; + @Mock + private PartitionService partitionService; + @Mock private MailExecutorService mailExecutor; - - TenantId tenantId = TenantId.fromUUID(UUID.fromString("00797a3b-7aeb-4b5b-b57a-c2a810d0f112")); @Spy @InjectMocks DefaultTbApiUsageStateService service; + private ApiUsageState dummyUsageState; + private TenantApiUsageState tenantApiUsageState; + private Tenant dummyTenant; + + private static final int MAX_ENABLE_VALUE = 5000; + private static final double WARN_THRESHOLD_VALUE = 0.8; + @BeforeEach public void setUp() { + var tenantId = TenantId.fromUUID(UUID.randomUUID()); + dummyTenant = new Tenant(); + dummyTenant.setId(tenantId); + dummyTenant.setEmail("test@tenant.com"); + + lenient().when(tsWsService.saveTimeseriesInternal(any())).thenReturn(Futures.immediateFuture(0)); + ReflectionTestUtils.setField(service, "tsWsService", tsWsService); + + DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() + .maxJSExecutions(MAX_ENABLE_VALUE) + .maxTransportMessages(MAX_ENABLE_VALUE) + .maxRuleChains(MAX_ENABLE_VALUE) + .maxTbelExecutions(MAX_ENABLE_VALUE) + .maxDPStorageDays(MAX_ENABLE_VALUE) + .maxREExecutions(MAX_ENABLE_VALUE) + .maxEmails(MAX_ENABLE_VALUE) + .maxSms(MAX_ENABLE_VALUE) + .maxCreatedAlarms(MAX_ENABLE_VALUE) + .warnThreshold(WARN_THRESHOLD_VALUE) + .build(); + + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(config); + + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setId(new TenantProfileId(UUID.randomUUID())); + tenantProfile.setProfileData(profileData); + + dummyUsageState = new ApiUsageState(); + dummyUsageState.setTransportState(ApiUsageStateValue.ENABLED); + dummyUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); + dummyUsageState.setReExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setJsExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); + dummyUsageState.setTenantId(tenantId); + dummyUsageState.setEntityId(tenantId); + dummyUsageState.setVersion(1L); + + tenantApiUsageState = new TenantApiUsageState(tenantProfile, dummyUsageState); + + service.myUsageStates.put(tenantId, tenantApiUsageState); + } + + @AfterEach + public void tearDown() { + } @Test public void givenTenantIdFromEntityStatesMap_whenGetApiUsageState() { - service.myUsageStates.put(tenantId, tenantUsageStateMock); - ApiUsageState tenantUsageState = service.getApiUsageState(tenantId); + service.myUsageStates.put(dummyTenant.getTenantId(), tenantUsageStateMock); + ApiUsageState tenantUsageState = service.getApiUsageState(dummyTenant.getTenantId()); assertThat(tenantUsageState, is(tenantUsageStateMock.getApiUsageState())); - Mockito.verify(service, never()).getOrFetchState(tenantId, tenantId); + Mockito.verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); } @Test - public void testAllApiFeaturesDisabledWhenLimitReached() { - doReturn(null).when(tsWsService).saveTimeseriesInternal(any()); - - TenantApiUsageState tenantUsageStateMock = mock(TenantApiUsageState.class); - ApiUsageState apiUsageState = getApiUsageState(); - when(tenantUsageStateMock.getApiUsageState()).thenReturn(apiUsageState); - - doReturn(BaseApiUsageState.StatsCalculationResult.builder() - .newValue(200L) - .valueChanged(true) - .newHourlyValue(200L) - .hourlyValueChanged(true) - .build()) - .when(tenantUsageStateMock).calculate(any(ApiUsageRecordKey.class), anyLong(), anyString()); - - doReturn(200L).when(tenantUsageStateMock).getProfileThreshold(any(ApiUsageRecordKey.class)); - doReturn(50L).when(tenantUsageStateMock).getProfileWarnThreshold(any(ApiUsageRecordKey.class)); - doReturn(300L).when(tenantUsageStateMock).get(any(ApiUsageRecordKey.class)); - - when(tenantUsageStateMock.getEntityType()).thenReturn(EntityType.TENANT); - when(tenantUsageStateMock.getEntityId()).thenReturn(tenantId); - - Map expectedResult = getExpectedResult(); - when(tenantUsageStateMock.checkStateUpdatedDueToThreshold(any())).thenReturn(expectedResult); - service.myUsageStates.put(tenantId, tenantUsageStateMock); - - when(apiUsageStateService.update(apiUsageState)).thenReturn(apiUsageState); - - Tenant dummyTenant = new Tenant(); - dummyTenant.setEmail("test@example.com"); - when(tenantService.findTenantById(any())).thenReturn(dummyTenant); - - TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder(); - UUID uuid = tenantId.getId(); - msgBuilder.setTenantIdMSB(uuid.getMostSignificantBits()) - .setTenantIdLSB(uuid.getLeastSignificantBits()); - msgBuilder.setCustomerIdMSB(0) - .setCustomerIdLSB(0); - msgBuilder.setServiceId("TEST_SERVICE"); - - List usageStats = new ArrayList<>(); - for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.values()) { - if (recordKey.getApiFeature() != null) { - TransportProtos.UsageStatsKVProto stat = TransportProtos.UsageStatsKVProto.newBuilder() - .setKey(recordKey.name()) - .setValue(1000L) - .build(); - usageStats.add(stat); - msgBuilder.addValues(stat); + public void testProcess_AllFeaturesTransitionToWarning() { + TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() + .setTenantIdMSB(dummyTenant.getId().getId().getMostSignificantBits()) + .setTenantIdLSB(dummyTenant.getId().getId().getLeastSignificantBits()) + .setCustomerIdMSB(0) + .setCustomerIdLSB(0) + .setServiceId("testService"); + + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + if (key.getApiFeature() != null) { + msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(key.name()) + .setValue(4500L) + .build()); } } TransportProtos.ToUsageStatsServiceMsg statsMsg = msgBuilder.build(); - TbProtoQueueMsg queueMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); + TbProtoQueueMsg msg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); TbCallback callback = mock(TbCallback.class); - service.process(queueMsg, callback); - verify(callback).onSuccess(); + when(apiUsageStateService.update(any(ApiUsageState.class))).thenAnswer(invocation -> { + ApiUsageState state = invocation.getArgument(0); + state.setVersion(2L); + return state; + }); + + when(tenantService.findTenantById(dummyTenant.getTenantId())).thenReturn(dummyTenant); - for (ApiFeature feature : expectedResult.keySet()) { - verify(notificationRuleProcessor, atLeastOnce()).process(argThat(trigger -> - trigger instanceof ApiUsageLimitTrigger && - ((ApiUsageLimitTrigger) trigger).getStatus() == ApiUsageStateValue.DISABLED && - ((ApiUsageLimitTrigger) trigger).getState().getApiFeature().getApiStateKey().equals(feature.getApiStateKey()) - )); + service.process(msg, callback); + + verify(callback, times(1)).onSuccess(); + + for (ApiFeature feature : ApiFeature.values()) { + if (containsFeature(feature)) { + assertEquals(ApiUsageStateValue.WARNING, tenantApiUsageState.getFeatureValue(feature), + "For feature " + feature + " expected state WARNING"); + } } - } + + assertEquals(ApiUsageStateValue.WARNING, tenantApiUsageState.getFeatureValue(ApiFeature.JS)); - @NotNull - private ApiUsageState getApiUsageState() { - ApiUsageState apiUsageState = new ApiUsageState(); - - apiUsageState.setTenantId(tenantId); - apiUsageState.setTransportState(ApiUsageStateValue.ENABLED); - apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); - apiUsageState.setReExecState(ApiUsageStateValue.ENABLED); - apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED); - apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); - apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); - apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); - apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); - return apiUsageState; + verify(notificationRuleProcessor, atLeastOnce()).process(any()); + verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); } - private Map getExpectedResult() { - Map expectedResult = new HashMap<>(); - for (ApiUsageRecordKey recordKey : ApiUsageRecordKey.values()) { - if (recordKey.getApiFeature() != null) { - expectedResult.put(recordKey.getApiFeature(), ApiUsageStateValue.DISABLED); + @Test + public void testProcess_AllFeaturesTransitionToDisabled() { + TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() + .setTenantIdMSB(dummyTenant.getId().getId().getMostSignificantBits()) + .setTenantIdLSB(dummyTenant.getId().getId().getLeastSignificantBits()) + .setCustomerIdMSB(0) + .setCustomerIdLSB(0) + .setServiceId("testService"); + + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + if (key.getApiFeature() != null) { + msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(key.name()) + .setValue(5500L) + .build()); + } + } + TransportProtos.ToUsageStatsServiceMsg statsMsg = msgBuilder.build(); + TbProtoQueueMsg msg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); + TbCallback callback = mock(TbCallback.class); + + when(apiUsageStateService.update(any(ApiUsageState.class))).thenAnswer(invocation -> { + ApiUsageState state = invocation.getArgument(0); + state.setVersion(2L); + return state; + }); + when(tenantService.findTenantById(dummyTenant.getTenantId())).thenReturn(dummyTenant); + + service.process(msg, callback); + verify(callback, times(1)).onSuccess(); + + for (ApiFeature feature : ApiFeature.values()) { + if (containsFeature(feature)) { + assertEquals(ApiUsageStateValue.DISABLED, tenantApiUsageState.getFeatureValue(feature), + "For feature " + feature + " expected state DISABLED"); } } + verify(notificationRuleProcessor, atLeastOnce()).process(any()); + verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); + } - return expectedResult; + private boolean containsFeature(ApiFeature feature) { + for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { + if (key.getApiFeature() != null && key.getApiFeature().equals(feature)) { + return true; + } + } + return false; } + } \ No newline at end of file From 9792c28545d5070431a6b77fcec4415e4bf1171e Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 17 Feb 2025 16:13:09 +0200 Subject: [PATCH 4/9] Refactored import format --- .../apiusage/DefaultTbApiUsageStateServiceTest.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 100c98adec..c9abb8e1c0 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -46,8 +46,14 @@ import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class DefaultTbApiUsageStateServiceTest { @@ -143,7 +149,7 @@ public class DefaultTbApiUsageStateServiceTest { service.myUsageStates.put(dummyTenant.getTenantId(), tenantUsageStateMock); ApiUsageState tenantUsageState = service.getApiUsageState(dummyTenant.getTenantId()); assertThat(tenantUsageState, is(tenantUsageStateMock.getApiUsageState())); - Mockito.verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); + verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); } @Test From 5ced8457f16241b71894d31b5a471d25159b37bf Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 17 Feb 2025 20:53:49 +0200 Subject: [PATCH 5/9] Added new test cases --- .../DefaultTbApiUsageStateServiceTest.java | 168 +++++++++++++++--- 1 file changed, 144 insertions(+), 24 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index c9abb8e1c0..8fefde0f6c 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -16,13 +16,12 @@ package org.thingsboard.server.service.apiusage; import com.google.common.util.concurrent.Futures; -import org.junit.jupiter.api.AfterEach; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; @@ -33,6 +32,8 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.gen.transport.TransportProtos; @@ -46,8 +47,11 @@ import java.util.UUID; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -58,9 +62,6 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class DefaultTbApiUsageStateServiceTest { - @Mock - TenantApiUsageState tenantUsageStateMock; - @Mock private NotificationRuleProcessor notificationRuleProcessor; @@ -73,21 +74,27 @@ public class DefaultTbApiUsageStateServiceTest { @Mock private InternalTelemetryService tsWsService; + @Mock + private MailExecutorService mailExecutor; + @Mock private PartitionService partitionService; @Mock - private MailExecutorService mailExecutor; + private TbTenantProfileCache tbTenantProfileCache; @Spy @InjectMocks DefaultTbApiUsageStateService service; - private ApiUsageState dummyUsageState; private TenantApiUsageState tenantApiUsageState; private Tenant dummyTenant; private static final int MAX_ENABLE_VALUE = 5000; + private static final long VALUE_WARNING = 4500L; + private static final long VALUE_DISABLE = 5500L; + private static final int NEW_MAX_ENABLE_VALUE = 4000; + private static final double NEW_WARN_THRESHOLD_VALUE = 0.9; private static final double WARN_THRESHOLD_VALUE = 0.8; @BeforeEach @@ -117,11 +124,20 @@ public class DefaultTbApiUsageStateServiceTest { TenantProfileData profileData = new TenantProfileData(); profileData.setConfiguration(config); - TenantProfile tenantProfile = new TenantProfile(); - tenantProfile.setId(new TenantProfileId(UUID.randomUUID())); - tenantProfile.setProfileData(profileData); + TenantProfile dummyTenantProfile = new TenantProfile(); + dummyTenantProfile.setId(new TenantProfileId(UUID.randomUUID())); + dummyTenantProfile.setProfileData(profileData); + + ApiUsageState dummyUsageState = getApiUsageState(tenantId); + + tenantApiUsageState = new TenantApiUsageState(dummyTenantProfile, dummyUsageState); - dummyUsageState = new ApiUsageState(); + service.myUsageStates.put(tenantId, tenantApiUsageState); + } + + @NotNull + private static ApiUsageState getApiUsageState(TenantId tenantId) { + ApiUsageState dummyUsageState = new ApiUsageState(); dummyUsageState.setTransportState(ApiUsageStateValue.ENABLED); dummyUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); dummyUsageState.setReExecState(ApiUsageStateValue.ENABLED); @@ -133,23 +149,61 @@ public class DefaultTbApiUsageStateServiceTest { dummyUsageState.setTenantId(tenantId); dummyUsageState.setEntityId(tenantId); dummyUsageState.setVersion(1L); + return dummyUsageState; + } - tenantApiUsageState = new TenantApiUsageState(tenantProfile, dummyUsageState); - - service.myUsageStates.put(tenantId, tenantApiUsageState); + @Test + public void givenTenantIdFromEntityStatesMap_whenGetApiUsageState() { + ApiUsageState tenantUsageState = service.getApiUsageState(dummyTenant.getTenantId()); + assertThat(tenantUsageState, is(tenantApiUsageState.getApiUsageState())); + verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); } - @AfterEach - public void tearDown() { + @Test + public void testGetApiUsageState_fromOtherUsageStates() { + TenantId tenantId = dummyTenant.getTenantId(); + service.myUsageStates.remove(tenantId); + ApiUsageState otherState = new ApiUsageState(); + otherState.setTenantId(tenantId); + service.otherUsageStates.put(tenantId, otherState); + ApiUsageState result = service.getApiUsageState(tenantId); + assertThat(result, is(otherState)); + } + @Test + public void testGetApiUsageState_whenMyPartition() { + TenantId tenantId = dummyTenant.getTenantId(); + service.myUsageStates.remove(tenantId); + service.otherUsageStates.remove(tenantId); + + TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); + when(tpi.isMyPartition()).thenReturn(true); + when(partitionService.resolve(any(), eq(dummyTenant.getId()), eq(dummyTenant.getId()))).thenReturn(tpi); + + TenantApiUsageState fetchedState = tenantApiUsageState; + doReturn(fetchedState).when(service).getOrFetchState(tenantId, tenantId); + ApiUsageState result = service.getApiUsageState(tenantId); + assertThat(result, is(fetchedState.getApiUsageState())); } @Test - public void givenTenantIdFromEntityStatesMap_whenGetApiUsageState() { - service.myUsageStates.put(dummyTenant.getTenantId(), tenantUsageStateMock); - ApiUsageState tenantUsageState = service.getApiUsageState(dummyTenant.getTenantId()); - assertThat(tenantUsageState, is(tenantUsageStateMock.getApiUsageState())); - verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); + public void testGetApiUsageState_whenNotMyPartition() { + TenantId tenantId = dummyTenant.getTenantId(); + service.myUsageStates.remove(tenantId); + service.otherUsageStates.remove(tenantId); + + TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); + when(tpi.isMyPartition()).thenReturn(false); + when(partitionService.resolve(any(), eq(tenantId), eq(tenantId))).thenReturn(tpi); + + ApiUsageState foundState = new ApiUsageState(); + foundState.setTenantId(tenantId); + when(apiUsageStateService.findTenantApiUsageState(tenantId)).thenReturn(foundState); + + ApiUsageState result = service.getApiUsageState(tenantId); + assertThat(result, is(foundState)); + + assertThat(service.otherUsageStates.get(tenantId), is(foundState)); } @Test @@ -165,7 +219,7 @@ public class DefaultTbApiUsageStateServiceTest { if (key.getApiFeature() != null) { msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() .setKey(key.name()) - .setValue(4500L) + .setValue(VALUE_WARNING) .build()); } } @@ -195,7 +249,6 @@ public class DefaultTbApiUsageStateServiceTest { assertEquals(ApiUsageStateValue.WARNING, tenantApiUsageState.getFeatureValue(ApiFeature.JS)); - verify(notificationRuleProcessor, atLeastOnce()).process(any()); verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); } @@ -213,7 +266,7 @@ public class DefaultTbApiUsageStateServiceTest { if (key.getApiFeature() != null) { msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() .setKey(key.name()) - .setValue(5500L) + .setValue(VALUE_DISABLE) .build()); } } @@ -241,6 +294,73 @@ public class DefaultTbApiUsageStateServiceTest { verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); } + @Test + public void testOnTenantProfileUpdate_updatesStateForMatchingTenant() { + TenantProfileId currentProfileId = tenantApiUsageState.getTenantProfileId(); + + TenantProfileData newProfileData = new TenantProfileData(); + DefaultTenantProfileConfiguration newConfig = DefaultTenantProfileConfiguration.builder() + .maxJSExecutions(NEW_MAX_ENABLE_VALUE) + .maxTransportMessages(MAX_ENABLE_VALUE) + .maxRuleChains(MAX_ENABLE_VALUE) + .maxTbelExecutions(MAX_ENABLE_VALUE) + .maxDPStorageDays(NEW_MAX_ENABLE_VALUE) + .maxREExecutions(MAX_ENABLE_VALUE) + .maxEmails(NEW_MAX_ENABLE_VALUE) + .maxSms(MAX_ENABLE_VALUE) + .maxCreatedAlarms(MAX_ENABLE_VALUE) + .warnThreshold(NEW_WARN_THRESHOLD_VALUE) + .build(); + + newProfileData.setConfiguration(newConfig); + + TenantProfile newProfile = new TenantProfile(); + newProfile.setId(currentProfileId); + newProfile.setProfileData(newProfileData); + + when(tbTenantProfileCache.get(currentProfileId)).thenReturn(newProfile); + + service.onTenantProfileUpdate(currentProfileId); + + assertEquals(currentProfileId, tenantApiUsageState.getTenantProfileId()); + assertEquals(newProfileData, tenantApiUsageState.getTenantProfileData()); + } + + @Test + public void testOnTenantUpdate_updatesStateWhenProfileChanged() { + TenantProfileId oldProfileId = tenantApiUsageState.getTenantProfileId(); + + TenantProfileId newProfileId = new TenantProfileId(UUID.randomUUID()); + TenantProfileData newProfileData = new TenantProfileData(); + DefaultTenantProfileConfiguration newConfig = DefaultTenantProfileConfiguration.builder() + .maxJSExecutions(NEW_MAX_ENABLE_VALUE) + .maxTransportMessages(MAX_ENABLE_VALUE) + .maxRuleChains(MAX_ENABLE_VALUE) + .maxTbelExecutions(NEW_MAX_ENABLE_VALUE) + .maxDPStorageDays(MAX_ENABLE_VALUE) + .maxREExecutions(NEW_MAX_ENABLE_VALUE) + .maxEmails(MAX_ENABLE_VALUE) + .maxSms(MAX_ENABLE_VALUE) + .maxCreatedAlarms(MAX_ENABLE_VALUE) + .warnThreshold(NEW_WARN_THRESHOLD_VALUE) + .build(); + + newProfileData.setConfiguration(newConfig); + + TenantProfile newProfile = new TenantProfile(); + newProfile.setId(newProfileId); + newProfile.setProfileData(newProfileData); + + when(tbTenantProfileCache.get(dummyTenant.getTenantId())).thenReturn(newProfile); + + assertNotEquals(oldProfileId, newProfileId); + + service.onTenantUpdate(dummyTenant.getTenantId()); + + assertEquals(newProfileId, tenantApiUsageState.getTenantProfileId()); + assertEquals(newProfileData, tenantApiUsageState.getTenantProfileData()); + } + private boolean containsFeature(ApiFeature feature) { for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { if (key.getApiFeature() != null && key.getApiFeature().equals(feature)) { From a118c44cf51d0f1daa0a8896e0d5fb490675c5d8 Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Wed, 19 Feb 2025 18:26:34 +0200 Subject: [PATCH 6/9] Added tests on api usage --- .../server/service/apiusage/ApiUsageTest.java | 144 +++++++ .../DefaultTbApiUsageStateServiceTest.java | 358 +++--------------- 2 files changed, 199 insertions(+), 303 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java new file mode 100644 index 0000000000..c708525a1d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.apiusage; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.controller.TbUrlConstants; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; + +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +@TestPropertySource(properties = { + "usage.stats.report.enabled=true", + "usage.stats.report.interval=2", + "usage.stats.gauge_report_interval=1", +}) +public class ApiUsageTest extends AbstractControllerTest { + + private Tenant savedTenant; + private User tenantAdmin; + + private static final int MAX_DP_ENABLE_VALUE = 12; + private static final double WARN_THRESHOLD_VALUE = 0.5; + @Autowired + private ApiUsageStateService apiUsageStateService; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + TenantProfile tenantProfile = createTenantProfile(); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + assertNotNull(savedTenantProfile); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + tenant.setTenantProfileId(savedTenantProfile.getId()); + savedTenant = saveTenant(tenant); + tenantId = savedTenant.getId(); + assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @Test + public void testTelemetryApiCall() throws Exception { + Device device = createDevice(); + assertNotNull(device); + String telemetryPayload = "{\"temperature\":25, \"humidity\":60}"; + String url = TbUrlConstants.TELEMETRY_URL_PREFIX + "/DEVICE/" + device.getId() + "/timeseries/ANY"; + + long VALUE_WARNING = (long) (MAX_DP_ENABLE_VALUE * WARN_THRESHOLD_VALUE) / 2; + + for (int i = 0; i < VALUE_WARNING; i++) { + doPostAsync(url, telemetryPayload, String.class, status().isOk()); + } + + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState())); + + long VALUE_DISABLE = (long) (MAX_DP_ENABLE_VALUE - (MAX_DP_ENABLE_VALUE * WARN_THRESHOLD_VALUE)) / 2; + + for (int i = 0; i < VALUE_DISABLE; i++) { + doPostAsync(url, telemetryPayload, String.class, status().isOk()); + } + + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> { + assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); + }); + } + + private TenantProfile createTenantProfile() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Tenant Profile"); + tenantProfile.setDescription("Tenant Profile" + " Test"); + + TenantProfileData tenantProfileData = new TenantProfileData(); + DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() + .maxDPStorageDays(MAX_DP_ENABLE_VALUE) + .warnThreshold(WARN_THRESHOLD_VALUE) + .build(); + + tenantProfileData.setConfiguration(config); + tenantProfile.setProfileData(tenantProfileData); + return tenantProfile; + } + + private Device createDevice() throws Exception { + String testToken = "TEST_TOKEN"; + + Device device = new Device(); + device.setName("My device"); + device.setType("default"); + device.setTenantId(tenantId); + + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + deviceCredentials.setCredentialsId(testToken); + + SaveDeviceWithCredentialsRequest saveRequest = new SaveDeviceWithCredentialsRequest(device, deviceCredentials); + + return readResponse(doPost("/api/device-with-credentials", saveRequest).andExpect(status().isOk()), Device.class); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 8fefde0f6c..8d4c1b849b 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -15,359 +15,111 @@ */ package org.thingsboard.server.service.apiusage; -import com.google.common.util.concurrent.Futures; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.service.mail.MailExecutorService; -import org.thingsboard.server.service.telemetry.InternalTelemetryService; import java.util.UUID; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.Assert.assertEquals; -@ExtendWith(MockitoExtension.class) -public class DefaultTbApiUsageStateServiceTest { +@DaoSqlTest +public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { - @Mock - private NotificationRuleProcessor notificationRuleProcessor; + @Autowired + DefaultTbApiUsageStateService service; - @Mock + @Autowired private ApiUsageStateService apiUsageStateService; - @Mock - private TenantService tenantService; - - @Mock - private InternalTelemetryService tsWsService; - - @Mock - private MailExecutorService mailExecutor; - - @Mock - private PartitionService partitionService; - - @Mock - private TbTenantProfileCache tbTenantProfileCache; - - @Spy - @InjectMocks - DefaultTbApiUsageStateService service; - - private TenantApiUsageState tenantApiUsageState; - private Tenant dummyTenant; + private TenantId tenantId; + private Tenant savedTenant; private static final int MAX_ENABLE_VALUE = 5000; private static final long VALUE_WARNING = 4500L; private static final long VALUE_DISABLE = 5500L; - private static final int NEW_MAX_ENABLE_VALUE = 4000; - private static final double NEW_WARN_THRESHOLD_VALUE = 0.9; private static final double WARN_THRESHOLD_VALUE = 0.8; - @BeforeEach - public void setUp() { - var tenantId = TenantId.fromUUID(UUID.randomUUID()); - dummyTenant = new Tenant(); - dummyTenant.setId(tenantId); - dummyTenant.setEmail("test@tenant.com"); - - lenient().when(tsWsService.saveTimeseriesInternal(any())).thenReturn(Futures.immediateFuture(0)); - - ReflectionTestUtils.setField(service, "tsWsService", tsWsService); - - DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() - .maxJSExecutions(MAX_ENABLE_VALUE) - .maxTransportMessages(MAX_ENABLE_VALUE) - .maxRuleChains(MAX_ENABLE_VALUE) - .maxTbelExecutions(MAX_ENABLE_VALUE) - .maxDPStorageDays(MAX_ENABLE_VALUE) - .maxREExecutions(MAX_ENABLE_VALUE) - .maxEmails(MAX_ENABLE_VALUE) - .maxSms(MAX_ENABLE_VALUE) - .maxCreatedAlarms(MAX_ENABLE_VALUE) - .warnThreshold(WARN_THRESHOLD_VALUE) - .build(); - - TenantProfileData profileData = new TenantProfileData(); - profileData.setConfiguration(config); - - TenantProfile dummyTenantProfile = new TenantProfile(); - dummyTenantProfile.setId(new TenantProfileId(UUID.randomUUID())); - dummyTenantProfile.setProfileData(profileData); + @Before + public void init() throws Exception { + loginSysAdmin(); - ApiUsageState dummyUsageState = getApiUsageState(tenantId); + TenantProfile tenantProfile = createTenantProfile(); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + Assert.assertNotNull(savedTenantProfile); - tenantApiUsageState = new TenantApiUsageState(dummyTenantProfile, dummyUsageState); - - service.myUsageStates.put(tenantId, tenantApiUsageState); - } - - @NotNull - private static ApiUsageState getApiUsageState(TenantId tenantId) { - ApiUsageState dummyUsageState = new ApiUsageState(); - dummyUsageState.setTransportState(ApiUsageStateValue.ENABLED); - dummyUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); - dummyUsageState.setReExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setJsExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); - dummyUsageState.setTenantId(tenantId); - dummyUsageState.setEntityId(tenantId); - dummyUsageState.setVersion(1L); - return dummyUsageState; - } - - @Test - public void givenTenantIdFromEntityStatesMap_whenGetApiUsageState() { - ApiUsageState tenantUsageState = service.getApiUsageState(dummyTenant.getTenantId()); - assertThat(tenantUsageState, is(tenantApiUsageState.getApiUsageState())); - verify(service, never()).getOrFetchState(dummyTenant.getTenantId(), dummyTenant.getTenantId()); - } - - @Test - public void testGetApiUsageState_fromOtherUsageStates() { - TenantId tenantId = dummyTenant.getTenantId(); - service.myUsageStates.remove(tenantId); - ApiUsageState otherState = new ApiUsageState(); - otherState.setTenantId(tenantId); - service.otherUsageStates.put(tenantId, otherState); - ApiUsageState result = service.getApiUsageState(tenantId); - assertThat(result, is(otherState)); + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + tenant.setTenantProfileId(savedTenantProfile.getId()); + savedTenant = saveTenant(tenant); + tenantId = savedTenant.getId(); + Assert.assertNotNull(savedTenant); } @Test - public void testGetApiUsageState_whenMyPartition() { - TenantId tenantId = dummyTenant.getTenantId(); - service.myUsageStates.remove(tenantId); - service.otherUsageStates.remove(tenantId); - - TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); - when(tpi.isMyPartition()).thenReturn(true); - when(partitionService.resolve(any(), eq(dummyTenant.getId()), eq(dummyTenant.getId()))).thenReturn(tpi); - - TenantApiUsageState fetchedState = tenantApiUsageState; - doReturn(fetchedState).when(service).getOrFetchState(tenantId, tenantId); - ApiUsageState result = service.getApiUsageState(tenantId); - assertThat(result, is(fetchedState.getApiUsageState())); - } - - @Test - public void testGetApiUsageState_whenNotMyPartition() { - TenantId tenantId = dummyTenant.getTenantId(); - service.myUsageStates.remove(tenantId); - service.otherUsageStates.remove(tenantId); - - TopicPartitionInfo tpi = mock(TopicPartitionInfo.class); - when(tpi.isMyPartition()).thenReturn(false); - when(partitionService.resolve(any(), eq(tenantId), eq(tenantId))).thenReturn(tpi); - - ApiUsageState foundState = new ApiUsageState(); - foundState.setTenantId(tenantId); - when(apiUsageStateService.findTenantApiUsageState(tenantId)).thenReturn(foundState); - - ApiUsageState result = service.getApiUsageState(tenantId); - assertThat(result, is(foundState)); - - assertThat(service.otherUsageStates.get(tenantId), is(foundState)); - } - - @Test - public void testProcess_AllFeaturesTransitionToWarning() { - TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() - .setTenantIdMSB(dummyTenant.getId().getId().getMostSignificantBits()) - .setTenantIdLSB(dummyTenant.getId().getId().getLeastSignificantBits()) + public void testProcess_transitionFromWarningToDisabled() { + TransportProtos.ToUsageStatsServiceMsg.Builder warningMsgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setCustomerIdMSB(0) .setCustomerIdLSB(0) .setServiceId("testService"); - for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - if (key.getApiFeature() != null) { - msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() - .setKey(key.name()) + warningMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) .setValue(VALUE_WARNING) .build()); - } - } - - TransportProtos.ToUsageStatsServiceMsg statsMsg = msgBuilder.build(); - TbProtoQueueMsg msg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); - TbCallback callback = mock(TbCallback.class); - - when(apiUsageStateService.update(any(ApiUsageState.class))).thenAnswer(invocation -> { - ApiUsageState state = invocation.getArgument(0); - state.setVersion(2L); - return state; - }); - - when(tenantService.findTenantById(dummyTenant.getTenantId())).thenReturn(dummyTenant); - - service.process(msg, callback); - - verify(callback, times(1)).onSuccess(); - for (ApiFeature feature : ApiFeature.values()) { - if (containsFeature(feature)) { - assertEquals(ApiUsageStateValue.WARNING, tenantApiUsageState.getFeatureValue(feature), - "For feature " + feature + " expected state WARNING"); - } - } + TransportProtos.ToUsageStatsServiceMsg warningStatsMsg = warningMsgBuilder.build(); + TbProtoQueueMsg warningMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), warningStatsMsg); - assertEquals(ApiUsageStateValue.WARNING, tenantApiUsageState.getFeatureValue(ApiFeature.JS)); + service.process(warningMsg, TbCallback.EMPTY); + assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); - verify(notificationRuleProcessor, atLeastOnce()).process(any()); - verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); - } - - @Test - public void testProcess_AllFeaturesTransitionToDisabled() { - TransportProtos.ToUsageStatsServiceMsg.Builder msgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() - .setTenantIdMSB(dummyTenant.getId().getId().getMostSignificantBits()) - .setTenantIdLSB(dummyTenant.getId().getId().getLeastSignificantBits()) + TransportProtos.ToUsageStatsServiceMsg.Builder disableMsgBuilder = TransportProtos.ToUsageStatsServiceMsg.newBuilder() + .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) + .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setCustomerIdMSB(0) .setCustomerIdLSB(0) .setServiceId("testService"); - for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - if (key.getApiFeature() != null) { - msgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() - .setKey(key.name()) + disableMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) .setValue(VALUE_DISABLE) .build()); - } - } - TransportProtos.ToUsageStatsServiceMsg statsMsg = msgBuilder.build(); - TbProtoQueueMsg msg = new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg); - TbCallback callback = mock(TbCallback.class); - - when(apiUsageStateService.update(any(ApiUsageState.class))).thenAnswer(invocation -> { - ApiUsageState state = invocation.getArgument(0); - state.setVersion(2L); - return state; - }); - when(tenantService.findTenantById(dummyTenant.getTenantId())).thenReturn(dummyTenant); - service.process(msg, callback); - verify(callback, times(1)).onSuccess(); + TransportProtos.ToUsageStatsServiceMsg disableStatsMsg = disableMsgBuilder.build(); + TbProtoQueueMsg disableMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), disableStatsMsg); - for (ApiFeature feature : ApiFeature.values()) { - if (containsFeature(feature)) { - assertEquals(ApiUsageStateValue.DISABLED, tenantApiUsageState.getFeatureValue(feature), - "For feature " + feature + " expected state DISABLED"); - } - } - verify(notificationRuleProcessor, atLeastOnce()).process(any()); - verify(mailExecutor, atLeastOnce()).submit((Runnable) any()); + service.process(disableMsg, TbCallback.EMPTY); + assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); } - @Test - public void testOnTenantProfileUpdate_updatesStateForMatchingTenant() { - TenantProfileId currentProfileId = tenantApiUsageState.getTenantProfileId(); - - TenantProfileData newProfileData = new TenantProfileData(); - DefaultTenantProfileConfiguration newConfig = DefaultTenantProfileConfiguration.builder() - .maxJSExecutions(NEW_MAX_ENABLE_VALUE) - .maxTransportMessages(MAX_ENABLE_VALUE) - .maxRuleChains(MAX_ENABLE_VALUE) - .maxTbelExecutions(MAX_ENABLE_VALUE) - .maxDPStorageDays(NEW_MAX_ENABLE_VALUE) - .maxREExecutions(MAX_ENABLE_VALUE) - .maxEmails(NEW_MAX_ENABLE_VALUE) - .maxSms(MAX_ENABLE_VALUE) - .maxCreatedAlarms(MAX_ENABLE_VALUE) - .warnThreshold(NEW_WARN_THRESHOLD_VALUE) - .build(); - - newProfileData.setConfiguration(newConfig); + private TenantProfile createTenantProfile() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Tenant Profile"); + tenantProfile.setDescription("Tenant Profile" + " Test"); - TenantProfile newProfile = new TenantProfile(); - newProfile.setId(currentProfileId); - newProfile.setProfileData(newProfileData); - - when(tbTenantProfileCache.get(currentProfileId)).thenReturn(newProfile); - - service.onTenantProfileUpdate(currentProfileId); - - assertEquals(currentProfileId, tenantApiUsageState.getTenantProfileId()); - assertEquals(newProfileData, tenantApiUsageState.getTenantProfileData()); - } - - @Test - public void testOnTenantUpdate_updatesStateWhenProfileChanged() { - TenantProfileId oldProfileId = tenantApiUsageState.getTenantProfileId(); - - TenantProfileId newProfileId = new TenantProfileId(UUID.randomUUID()); - TenantProfileData newProfileData = new TenantProfileData(); - DefaultTenantProfileConfiguration newConfig = DefaultTenantProfileConfiguration.builder() - .maxJSExecutions(NEW_MAX_ENABLE_VALUE) - .maxTransportMessages(MAX_ENABLE_VALUE) - .maxRuleChains(MAX_ENABLE_VALUE) - .maxTbelExecutions(NEW_MAX_ENABLE_VALUE) + TenantProfileData tenantProfileData = new TenantProfileData(); + DefaultTenantProfileConfiguration config = DefaultTenantProfileConfiguration.builder() .maxDPStorageDays(MAX_ENABLE_VALUE) - .maxREExecutions(NEW_MAX_ENABLE_VALUE) - .maxEmails(MAX_ENABLE_VALUE) - .maxSms(MAX_ENABLE_VALUE) - .maxCreatedAlarms(MAX_ENABLE_VALUE) - .warnThreshold(NEW_WARN_THRESHOLD_VALUE) + .warnThreshold(WARN_THRESHOLD_VALUE) .build(); - newProfileData.setConfiguration(newConfig); - - TenantProfile newProfile = new TenantProfile(); - newProfile.setId(newProfileId); - newProfile.setProfileData(newProfileData); - - when(tbTenantProfileCache.get(dummyTenant.getTenantId())).thenReturn(newProfile); - - assertNotEquals(oldProfileId, newProfileId); - - service.onTenantUpdate(dummyTenant.getTenantId()); - - assertEquals(newProfileId, tenantApiUsageState.getTenantProfileId()); - assertEquals(newProfileData, tenantApiUsageState.getTenantProfileData()); - } - - private boolean containsFeature(ApiFeature feature) { - for (ApiUsageRecordKey key : ApiUsageRecordKey.values()) { - if (key.getApiFeature() != null && key.getApiFeature().equals(feature)) { - return true; - } - } - return false; + tenantProfileData.setConfiguration(config); + tenantProfile.setProfileData(tenantProfileData); + return tenantProfile; } } \ No newline at end of file From 28ee01a7dd926db85ea9144e777d39583426b68a Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 24 Feb 2025 13:47:11 +0200 Subject: [PATCH 7/9] Fixed DefaultTbApiUsageStateServiceTest file formatting --- .../DefaultTbApiUsageStateServiceTest.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 8d4c1b849b..4e53cfa449 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -19,7 +19,10 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; @@ -76,10 +79,10 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { .setCustomerIdLSB(0) .setServiceId("testService"); - warningMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() - .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) - .setValue(VALUE_WARNING) - .build()); + warningMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) + .setValue(VALUE_WARNING) + .build()); TransportProtos.ToUsageStatsServiceMsg warningStatsMsg = warningMsgBuilder.build(); TbProtoQueueMsg warningMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), warningStatsMsg); @@ -94,10 +97,10 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { .setCustomerIdLSB(0) .setServiceId("testService"); - disableMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() - .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) - .setValue(VALUE_DISABLE) - .build()); + disableMsgBuilder.addValues(TransportProtos.UsageStatsKVProto.newBuilder() + .setKey(ApiUsageRecordKey.STORAGE_DP_COUNT.name()) + .setValue(VALUE_DISABLE) + .build()); TransportProtos.ToUsageStatsServiceMsg disableStatsMsg = disableMsgBuilder.build(); TbProtoQueueMsg disableMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), disableStatsMsg); From 545ea89713bdf3b4f31b3275d42b5c5f5e5575ae Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 24 Feb 2025 14:59:54 +0200 Subject: [PATCH 8/9] Increased timeout to 30 seconds --- .../org/thingsboard/server/service/apiusage/ApiUsageTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java index c708525a1d..84a6fb9931 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java @@ -94,7 +94,7 @@ public class ApiUsageTest extends AbstractControllerTest { doPostAsync(url, telemetryPayload, String.class, status().isOk()); } - await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState())); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(ApiUsageStateValue.WARNING, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState())); long VALUE_DISABLE = (long) (MAX_DP_ENABLE_VALUE - (MAX_DP_ENABLE_VALUE * WARN_THRESHOLD_VALUE)) / 2; @@ -102,7 +102,7 @@ public class ApiUsageTest extends AbstractControllerTest { doPostAsync(url, telemetryPayload, String.class, status().isOk()); } - await().atMost(5, TimeUnit.SECONDS) + await().atMost(TIMEOUT, TimeUnit.SECONDS) .untilAsserted(() -> { assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); }); From 012aafbd82935eef0cad2f18ab55058b87497c7d Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Tue, 25 Feb 2025 12:02:25 +0200 Subject: [PATCH 9/9] Updated license --- .../org/thingsboard/server/service/apiusage/ApiUsageTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java index 84a6fb9931..961185d3cd 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/ApiUsageTest.java @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2024 The Thingsboard Authors + * Copyright © 2016-2025 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.