From d9dad36c54179b42d6cad4b66c82e6cab61436a8 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 15 Jan 2025 16:19:56 +0200 Subject: [PATCH 01/19] Send alarm_ack and alarm_clear only by 'Push to Edge' node --- .../edge/EdgeEventSourcingListener.java | 3 ++ .../server/edge/AlarmEdgeTest.java | 33 +++++-------------- .../engine/edge/AbstractTbMsgPushNode.java | 12 +++++-- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java index 9317c51a64..02b2346e64 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeEventSourcingListener.java @@ -135,6 +135,9 @@ public class EdgeEventSourcingListener { if (EntityType.DEVICE.equals(event.getEntityId().getEntityType()) && ActionType.ASSIGNED_TO_TENANT.equals(event.getActionType())) { return; } + if (EntityType.ALARM.equals(event.getEntityId().getEntityType())) { + return; + } try { if (event.getEntityId().getEntityType().equals(EntityType.RULE_CHAIN) && event.getEdgeId() != null && event.getActionType().equals(ActionType.ASSIGNED_TO_EDGE)) { try { diff --git a/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java index 239cb87ab4..0ceebf1cf4 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AlarmEdgeTest.java @@ -94,44 +94,26 @@ public class AlarmEdgeTest extends AbstractEdgeTest { Alarm savedAlarm = doPost("/api/alarm", alarm, Alarm.class); edgeImitator.ignoreType(AlarmCommentUpdateMsg.class); - // ack alarm + // ack alarm - send only by using push to edge node edgeImitator.expectMessageAmount(1); doPost("/api/alarm/" + savedAlarm.getUuidId() + "/ack"); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); - AlarmUpdateMsg alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ALARM_ACK_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); - Alarm alarmMsg = JacksonUtil.fromString(alarmUpdateMsg.getEntity(), Alarm.class, true); - Assert.assertNotNull(alarmMsg); - Assert.assertEquals(savedAlarm.getType(), alarmMsg.getType()); - Assert.assertEquals(savedAlarm.getName(), alarmMsg.getName()); - Assert.assertEquals(AlarmStatus.ACTIVE_ACK, alarmMsg.getStatus()); + Assert.assertFalse(edgeImitator.waitForMessages(5)); - // clear alarm + // clear alarm - send only by using push to edge node edgeImitator.expectMessageAmount(1); doPost("/api/alarm/" + savedAlarm.getUuidId() + "/clear"); - Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); - alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ALARM_CLEAR_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); - alarmMsg = JacksonUtil.fromString(alarmUpdateMsg.getEntity(), Alarm.class, true); - Assert.assertNotNull(alarmMsg); - Assert.assertEquals(savedAlarm.getType(), alarmMsg.getType()); - Assert.assertEquals(savedAlarm.getName(), alarmMsg.getName()); - Assert.assertEquals(AlarmStatus.CLEARED_ACK, alarmMsg.getStatus()); + Assert.assertFalse(edgeImitator.waitForMessages(5)); // delete alarm edgeImitator.expectMessageAmount(1); doDelete("/api/alarm/" + savedAlarm.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof AlarmUpdateMsg); - alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; + AlarmUpdateMsg alarmUpdateMsg = (AlarmUpdateMsg) latestMessage; Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, alarmUpdateMsg.getMsgType()); - alarmMsg = JacksonUtil.fromString(alarmUpdateMsg.getEntity(), Alarm.class, true); + Alarm alarmMsg = JacksonUtil.fromString(alarmUpdateMsg.getEntity(), Alarm.class, true); Assert.assertNotNull(alarmMsg); Assert.assertEquals(savedAlarm.getType(), alarmMsg.getType()); Assert.assertEquals(savedAlarm.getName(), alarmMsg.getName()); @@ -252,4 +234,5 @@ public class AlarmEdgeTest extends AbstractEdgeTest { alarmComment.setCreatedTime(Uuids.unixTimestamp(uuid)); return alarmComment; } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index 73368d40f5..8ed0313d32 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -39,6 +39,8 @@ import java.util.UUID; import static org.thingsboard.server.common.data.msg.TbMsgType.ACTIVITY_EVENT; import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_ACK; +import static org.thingsboard.server.common.data.msg.TbMsgType.ALARM_CLEAR; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.CONNECT_EVENT; @@ -81,6 +83,9 @@ public abstract class AbstractTbMsgPushNode metadata = msg.getMetaData().getData(); EdgeEventActionType actionType = getEdgeEventActionTypeByMsgType(msg); @@ -164,8 +169,9 @@ public abstract class AbstractTbMsgPushNode Date: Wed, 15 Jan 2025 17:42:22 +0200 Subject: [PATCH 02/19] Fix AlarmControllerTest --- .../controller/AlarmControllerTest.java | 28 +++++++++---------- .../import-dialog.component.html | 3 +- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java index 2f264d49ff..6bd8396ceb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java @@ -169,7 +169,7 @@ public class AlarmControllerTest extends AbstractControllerTest { foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundAlarm, customerDevice, tenantId, - customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ACK, 1, 1, 1); + customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ACK, 1, 0, 1); Mockito.reset(tbClusterService, auditLogService); alarm = updatedAlarm; @@ -183,7 +183,7 @@ public class AlarmControllerTest extends AbstractControllerTest { foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundAlarm, customerDevice, tenantId, - customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_CLEAR, 1, 1, 1); + customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_CLEAR, 1, 0, 1); Mockito.reset(tbClusterService, auditLogService); alarm = updatedAlarm; @@ -197,7 +197,7 @@ public class AlarmControllerTest extends AbstractControllerTest { foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundAlarm, customerDevice, tenantId, - customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED, 1, 1, 1); + customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED, 1, 0, 1); Mockito.reset(tbClusterService, auditLogService); alarm = updatedAlarm; @@ -211,7 +211,7 @@ public class AlarmControllerTest extends AbstractControllerTest { foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(foundAlarm, customerDevice, tenantId, - customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED, 1, 1, 1); + customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED, 1, 0, 1); Mockito.reset(tbClusterService, auditLogService); } @@ -321,7 +321,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertNotNull(foundAlarm); Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus()); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_CLEAR); } @@ -337,7 +337,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertNotNull(foundAlarm); Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus()); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_CLEAR); } @@ -354,7 +354,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertNotNull(foundAlarm); Assert.assertEquals(AlarmStatus.ACTIVE_ACK, foundAlarm.getStatus()); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ACK); } @@ -434,7 +434,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED); } @@ -465,7 +465,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED); logout(); @@ -482,7 +482,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(customerUserId, foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGNED); } @@ -500,7 +500,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED); beforeAssignmentTs = System.currentTimeMillis(); @@ -511,7 +511,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertNull(foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED); } @@ -529,7 +529,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED); logout(); @@ -545,7 +545,7 @@ public class AlarmControllerTest extends AbstractControllerTest { Assert.assertNull(foundAlarm.getAssigneeId()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); - testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED); } diff --git a/ui-ngx/src/app/shared/import-export/import-dialog.component.html b/ui-ngx/src/app/shared/import-export/import-dialog.component.html index 8bbe1599c4..76e6f8efac 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog.component.html +++ b/ui-ngx/src/app/shared/import-export/import-dialog.component.html @@ -63,8 +63,7 @@ {{ 'action.cancel' | translate }} From 1bbb88b826e439f68970d9d5ff1340af3d8158a7 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 20 Jan 2025 16:21:24 +0200 Subject: [PATCH 03/19] Revert import-dialog back to default --- .../src/app/shared/import-export/import-dialog.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/import-export/import-dialog.component.html b/ui-ngx/src/app/shared/import-export/import-dialog.component.html index 76e6f8efac..8bbe1599c4 100644 --- a/ui-ngx/src/app/shared/import-export/import-dialog.component.html +++ b/ui-ngx/src/app/shared/import-export/import-dialog.component.html @@ -63,7 +63,8 @@ {{ 'action.cancel' | translate }} From ec8e7a07268694685cfb047f1fc7724a36e50b2d Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 10 Feb 2025 14:28:46 +0200 Subject: [PATCH 04/19] 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 05/19] 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 06/19] 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 07/19] 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 08/19] 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 09/19] 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 f3c93104f61c0ba7e7f18252442a0ae3b395f626 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 20 Feb 2025 16:53:35 +0100 Subject: [PATCH 10/19] fixed coap.enabled property config --- application/src/main/resources/thingsboard.yml | 5 +++-- .../server/transport/coap/AbstractCoapIntegrationTest.java | 2 +- .../coap/security/AbstractCoapSecurityIntegrationTest.java | 2 +- application/src/test/resources/application-test.properties | 2 +- .../thingsboard/server/coapserver/TbCoapServerComponent.java | 2 +- .../server/coapserver/TbCoapTransportComponent.java | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 27478c8a01..991e060f1f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1277,8 +1277,9 @@ transport: # CoAP server parameters coap: - # Enable/disable coap server. - enabled: "${COAP_SERVER_ENABLED:true}" + server: + # Enable/disable coap server. + enabled: "${COAP_SERVER_ENABLED:true}" # CoAP bind address bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" # CoAP bind port diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java index 2b0d87527b..931944521c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java @@ -45,7 +45,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @TestPropertySource(properties = { - "coap.enabled=true", + "coap.server.enabled=true", "service.integrations.supported=ALL", "transport.coap.enabled=true", }) diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java index 8413c6f32b..fea5b2484e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java @@ -61,7 +61,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Slf4j @TestPropertySource(properties = { - "coap.enabled=true", + "coap.server.enabled=true", "coap.dtls.enabled=true", "coap.dtls.credentials.pem.cert_file=coap/credentials/server/cert.pem", "device.connectivity.coaps.enabled=true", diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 9951caa876..a5cc50f5d1 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -19,7 +19,7 @@ transport.mqtt.enabled=false transport.coap.enabled=false transport.lwm2m.enabled=false transport.snmp.enabled=false -coap.enabled=false +coap.server.enabled=false integrations.rpc.enabled=false service.integrations.supported=NONE diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java index 2d94de2682..a5beca9142 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapServerComponent.java @@ -21,6 +21,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.enabled}'=='true')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.server.enabled}'=='true')") public @interface TbCoapServerComponent { } diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java index 33f4b36b70..765415f750 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapTransportComponent.java @@ -22,6 +22,6 @@ import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) @ConditionalOnExpression("'${service.type:null}'=='tb-transport' || " + - "('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.enabled}'=='true' && '${transport.coap.enabled}'=='true')") + "('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${coap.server.enabled}'=='true' && '${transport.coap.enabled}'=='true')") public @interface TbCoapTransportComponent { } From 28ee01a7dd926db85ea9144e777d39583426b68a Mon Sep 17 00:00:00 2001 From: Artem Barysh Date: Mon, 24 Feb 2025 13:47:11 +0200 Subject: [PATCH 11/19] 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 12/19] 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 13/19] 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. From 347e18ec4bf72c84b279f50a2a232710be3820be Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 25 Feb 2025 15:14:16 +0100 Subject: [PATCH 14/19] used correct queueName for TPI in input node --- .../actors/ruleChain/DefaultTbContext.java | 5 +- .../actors/rule/DefaultTbContextTest.java | 58 +++++++++++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 8bd3bcc81d..66afd2671c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -124,6 +124,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED; @@ -178,7 +179,7 @@ public class DefaultTbContext implements TbContext { .resetRuleNodeId() .build(); tbMsg.pushToStack(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId()); - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(msg); doEnqueue(tpi, tbMsg, new SimpleTbQueueCallback(md -> ack(msg), t -> tellFailure(msg, t))); } @@ -195,7 +196,7 @@ public class DefaultTbContext implements TbContext { @Override public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg, MAIN_QUEUE_NAME); enqueue(tpi, tbMsg, onFailure, onSuccess); } diff --git a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java index af2f9ce8ec..ea900afd4e 100644 --- a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java @@ -98,6 +98,56 @@ class DefaultTbContextTest { defaultTbContext = new DefaultTbContext(mainCtxMock, "Test rule chain name", nodeCtxMock); } + @MethodSource + @ParameterizedTest + public void givenMsgWithQueueName_whenInput_thenVerifyEnqueueWithCorrectTpi(String queueName) { + // GIVEN + var tpi = resolve(queueName); + + given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi); + var clusterService = mock(TbClusterService.class); + given(mainCtxMock.getClusterService()).willReturn(clusterService); + var callbackMock = mock(TbMsgCallback.class); + given(callbackMock.isMsgValid()).willReturn(true); + var ruleNode = new RuleNode(RULE_NODE_ID); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TENANT_ID) + .queueName(queueName) + .copyMetaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_STRING) + .callback(callbackMock) + .build(); + + var ruleChainId = new RuleChainId(UUID.randomUUID()); + + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugSettings(DebugSettings.failures()); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + + // WHEN + defaultTbContext.input(msg, ruleChainId); + + // THEN + + then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); + } + + private static Stream givenMsgWithQueueName_whenInput_thenVerifyEnqueueWithCorrectTpi() { + return Stream.of("Main", "Test", null); + } + + private TopicPartitionInfo resolve(String queueName) { + var tpiBuilder = TopicPartitionInfo.builder() + .topic(queueName == null ? "MainQueueTopic" : queueName + "QueueTopic") + .partition(1) + .myPartition(true); + + return tpiBuilder.build(); + } + @Test public void givenDebugFailuresEvents_whenTellSuccess_thenVerifyDebugOutputNotPersisted() { // GIVEN @@ -810,10 +860,10 @@ class DefaultTbContextTest { @MethodSource @ParameterizedTest void givenDebugFailuresAndDebugAllAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence(boolean debugFailures, - long debugAllUntil, - String connection, - boolean shouldPersist, - boolean shouldPersistAfterDurationTime) { + long debugAllUntil, + String connection, + boolean shouldPersist, + boolean shouldPersistAfterDurationTime) { // GIVEN var callbackMock = mock(TbMsgCallback.class); var msg = getTbMsgWithCallback(callbackMock); From 002ecc63424e8485271711df6c908cf425f2ae69 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 26 Feb 2025 11:16:05 +0200 Subject: [PATCH 15/19] used correct queueName for TPI in input node --- .../server/actors/ruleChain/DefaultTbContext.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 0070eba4a1..091b97ac9d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -124,6 +124,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED; @@ -175,7 +176,7 @@ public class DefaultTbContext implements TbContext { } TbMsg tbMsg = msg.copyWithRuleChainId(ruleChainId); tbMsg.pushToStack(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId()); - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(msg); doEnqueue(tpi, tbMsg, new SimpleTbQueueCallback(md -> ack(msg), t -> tellFailure(msg, t))); } @@ -192,7 +193,7 @@ public class DefaultTbContext implements TbContext { @Override public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getQueueName(), getTenantId(), tbMsg.getOriginator()); + TopicPartitionInfo tpi = resolvePartition(tbMsg, MAIN_QUEUE_NAME); enqueue(tpi, tbMsg, onFailure, onSuccess); } From 9d20d34357c4c3a8fdc0e9869eb234a289d3a7e8 Mon Sep 17 00:00:00 2001 From: nick Date: Sun, 16 Feb 2025 20:15:04 +0200 Subject: [PATCH 16/19] lwm2m: fix bug observe object with "ver = 1.1" (cherry picked from commit fcf74a82407ef54236e35eea97d6d838c531bc9f) --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 11 +- .../transport/lwm2m/Lwm2mTestHelper.java | 3 +- .../lwm2m/client/LwM2MTestClient.java | 21 +- ...LwM2MIntegrationObserve_Ver_1_0_Test.java} | 15 +- ...cLwM2MIntegrationObserve_Ver_1_1_Test.java | 28 ++ ...cLwM2MIntegrationObserve_Ver_1_2_Test.java | 28 ++ .../rpc/AbstractRpcLwM2MIntegrationTest.java | 6 +- ...cLwm2MIntegrationObserveCompositeTest.java | 4 +- .../sql/RpcLwm2mIntegrationObserveTest.java | 4 +- ...cLwm2mIntegrationObserve_Ver_1_0_Test.java | 48 +++ ...cLwm2mIntegrationObserve_Ver_1_1_Test.java | 46 +++ ...cLwm2mIntegrationObserve_Ver_1_2_Test.java | 48 +++ .../src/test/resources/lwm2m/3-1_0.xml | 327 +++++++++++++++++ .../src/test/resources/lwm2m/3-1_1.xml | 331 ++++++++++++++++++ .../lwm2m/server/client/LwM2mClient.java | 40 ++- 15 files changed, 924 insertions(+), 36 deletions(-) rename application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/{AbstractRpcLwM2MIntegrationObserveTest.java => AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java} (50%) create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java create mode 100644 application/src/test/resources/lwm2m/3-1_0.xml create mode 100644 application/src/test/resources/lwm2m/3-1_1.xml diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index a1fb723681..2c69ff6c97 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -89,6 +89,7 @@ import static org.awaitility.Awaitility.await; import static org.eclipse.leshan.client.object.Security.noSec; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -314,7 +315,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte public void createNewClient(Security security, Security securityBs, boolean isRpc, String endpoint, Integer clientDtlsCidLength, boolean queueMode, String deviceIdStr) throws Exception { this.clientDestroy(false); - lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint); + lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources); try (ServerSocket socket = new ServerSocket(0)) { int clientPort = socket.getLocalPort(); @@ -327,7 +328,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte private void clientDestroy(boolean isAfter) { try { - if (lwM2MTestClient != null) { + if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) { if (isAfter) { sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); awaitDeleteDevice(lwM2MTestClient.getDeviceIdStr()); @@ -425,9 +426,15 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); return rpcActualResult.get("value").asText(); } + protected void sendRpcObserveWithContainsLwM2mSingleResource(String params) throws Exception { + String rpcActualResult = sendRpcObserveOkWithResultValue("Observe", params); + assertTrue(rpcActualResult.contains("LwM2mSingleResource") || rpcActualResult.contains("LwM2mMultipleResource")); + } + protected String sendRpcObserveOk(String method, String params) throws Exception { return sendObserveOK(method, params, lwM2MTestClient.getDeviceIdStr()); } + protected String sendObserveOK(String method, String params, String deviceIdStr) throws Exception { String sendRpcRequest; if (params == null) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 4aa47eddf5..a40c937de8 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -17,8 +17,9 @@ package org.thingsboard.server.transport.lwm2m; public class Lwm2mTestHelper { + public static final String[] lwm2mClientResources = new String[]{"3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; + // Models - public static final String[] resources = new String[]{"0.xml", "1.xml", "2.xml", "3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; public static final int BINARY_APP_DATA_CONTAINER = 19; public static final int TEMPERATURE_SENSOR = 3303; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 387085c511..a2dab420f6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -116,7 +116,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INST import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.lwm2mClientResources; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.setDtlsConnectorConfigCidLength; @@ -126,6 +126,7 @@ public class LwM2MTestClient { private final ScheduledExecutorService executor; private final String endpoint; + private final String[] modelResources; private LeshanClient leshanClient; private SimpleLwM2MDevice lwM2MDevice; private FwLwM2MDevice fwLwM2MDevice; @@ -147,10 +148,24 @@ public class LwM2MTestClient { Assert.assertNull("client already initialized", leshanClient); this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; this.clientContext = clientContext; - List models = new ArrayList<>(); - for (String resourceName : resources) { + + List models = ObjectLoader.loadAllDefault(); + for (String resourceName : lwm2mClientResources) { models.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); } + if (this.modelResources != null) { + List modelsRes = new ArrayList<>(); + for (String resourceName : this.modelResources) { + modelsRes.addAll(ObjectLoader.loadDdfFile(LwM2MTestClient.class.getClassLoader().getResourceAsStream("lwm2m/" + resourceName), resourceName)); + } + Set idsToRemove = new HashSet<>(); + for (ObjectModel model : modelsRes) { + idsToRemove.add(model.id); + } + models.removeIf(model -> idsToRemove.contains(model.id)); + models.addAll(modelsRes); + } + LwM2mModel model = new StaticModel(models); ObjectsInitializer initializer = new ObjectsInitializer(model); if (securityBs != null && security != null) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java similarity index 50% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java index 7195b0d229..0b7faafd7b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test.java @@ -16,18 +16,13 @@ package org.thingsboard.server.transport.lwm2m.rpc; import org.thingsboard.server.dao.service.DaoSqlTest; -import static org.junit.Assert.assertTrue; @DaoSqlTest -public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest{ - private final String[] RESOURCES_RPC_MULTIPLE_19 = new String[]{"0.xml", "1.xml", "2.xml", "3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; +public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test extends AbstractRpcLwM2MIntegrationTest{ - public AbstractRpcLwM2MIntegrationObserveTest() { - setResources(this.RESOURCES_RPC_MULTIPLE_19); - } - - protected void sendRpcObserveWithContainsLwM2mSingleResource(String params) throws Exception { - String rpcActualResult = sendRpcObserveOkWithResultValue("Observe", params); - assertTrue(rpcActualResult.contains("LwM2mSingleResource") || rpcActualResult.contains("LwM2mMultipleResource")); + public AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test() { + String[] RESOURCES_RPC_VER_1_1 = new String[]{"3-1_0.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_RPC_VER_1_1); } } + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java new file mode 100644 index 0000000000..0a9290b0c4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test.java @@ -0,0 +1,28 @@ +/** + * 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.transport.lwm2m.rpc; + +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test extends AbstractRpcLwM2MIntegrationTest{ + + public AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test() { + String[] RESOURCES_RPC_VER_1_1 = new String[]{"3-1_1.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_RPC_VER_1_1); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java new file mode 100644 index 0000000000..e52ca15634 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test.java @@ -0,0 +1,28 @@ +/** + * 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.transport.lwm2m.rpc; + +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public abstract class AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test extends AbstractRpcLwM2MIntegrationTest{ + + public AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test() { + String[] RESOURCES_RPC_VER_1_1 = new String[]{"3.xml", "5.xml", "6.xml", "9.xml", "19.xml"}; + setResources(RESOURCES_RPC_VER_1_1); + } +} + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index d5c9683c39..de4845a220 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -63,7 +63,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.lwm2mClientResources; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @Slf4j @@ -102,7 +102,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected LwM2mTransportServerHelper lwM2mTransportServerHelperTest; public AbstractRpcLwM2MIntegrationTest() { - setResources(resources); + setResources(lwm2mClientResources); } @Before @@ -110,7 +110,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationWriteCborTest")) { supportFormatOnly_SenMLJSON_SenMLCBOR = true; } - if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationObserveTest")) { + if (this.getClass().getSimpleName().contains("RpcLwm2mIntegrationObserve")) { initRpc(0); } else if (this.getClass().getSimpleName().equals("RpcLwm2mIntegrationReadCollectedValueTest")) { initRpc(3303); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java index 56de08e66d..aa05b8073c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2MIntegrationObserveCompositeTest.java @@ -21,7 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -50,7 +50,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_IN import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @Slf4j -public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationObserveTest { +public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationTest { /** * ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0/0"]} - Ok diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index 57213e08cd..425c7a3d6b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -24,7 +24,7 @@ import org.eclipse.leshan.server.registration.Registration; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; -import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.junit.Assert.assertEquals; @@ -41,7 +41,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; @Slf4j -public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationObserveTest { +public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest { @Before public void setupObserveTest() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java new file mode 100644 index 0000000000..9c2fbd3313 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_0_Test.java @@ -0,0 +1,48 @@ +/** + * 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.transport.lwm2m.rpc.sql; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test; + +import static org.junit.Assert.assertTrue; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; + +@Slf4j +public class RpcLwm2mIntegrationObserve_Ver_1_0_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test { + + @Before + public void setupObserveTest() throws Exception { + awaitObserveReadAll(4,lwM2MTestClient.getDeviceIdStr()); + } + + /** + * Observe "3_1.0/0/9" + * @throws Exception + */ + @Test + public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { + long initSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); + sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9); + updateRegAtLeastOnceAfterAction(); + long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java new file mode 100644 index 0000000000..79d09721e9 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_1_Test.java @@ -0,0 +1,46 @@ +/** + * 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.transport.lwm2m.rpc.sql; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test; +import static org.junit.Assert.assertTrue; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; + +@Slf4j +public class RpcLwm2mIntegrationObserve_Ver_1_1_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_1_Test { + + @Before + public void setupObserveTest() throws Exception { + awaitObserveReadAll(4,lwM2MTestClient.getDeviceIdStr()); + } + + /** + * Observe "3_1.1/0/9" + * @throws Exception + */ + @Test + public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { + long initSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); + sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9); + updateRegAtLeastOnceAfterAction(); + long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java new file mode 100644 index 0000000000..ec45c2beac --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserve_Ver_1_2_Test.java @@ -0,0 +1,48 @@ +/** + * 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.transport.lwm2m.rpc.sql; + +import lombok.extern.slf4j.Slf4j; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_0_Test; +import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test; + +import static org.junit.Assert.assertTrue; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; + +@Slf4j +public class RpcLwm2mIntegrationObserve_Ver_1_2_Test extends AbstractRpcLwM2MIntegrationObserve_Ver_1_2_Test { + + @Before + public void setupObserveTest() throws Exception { + awaitObserveReadAll(4,lwM2MTestClient.getDeviceIdStr()); + } + + /** + * Observe "3_1.2/0/9" + * @throws Exception + */ + @Test + public void testObserveOneResource_Result_CONTENT_Value_Count_3_After_Cancel_Count_2() throws Exception { + long initSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + sendObserveCancelAllWithAwait(lwM2MTestClient.getDeviceIdStr()); + sendRpcObserveWithContainsLwM2mSingleResource(idVer_3_0_9); + updateRegAtLeastOnceAfterAction(); + long lastSendTelemetryAtCount = countSendParametersOnThingsboardTelemetryResource(RESOURCE_ID_NAME_3_9); + assertTrue(lastSendTelemetryAtCount > initSendTelemetryAtCount); + } +} diff --git a/application/src/test/resources/lwm2m/3-1_0.xml b/application/src/test/resources/lwm2m/3-1_0.xml new file mode 100644 index 0000000000..4f5261d263 --- /dev/null +++ b/application/src/test/resources/lwm2m/3-1_0.xml @@ -0,0 +1,327 @@ + + + + + + + Device + + 3 + urn:oma:lwm2m:oma:3 + Single + Mandatory + + + Manufacturer + R + Single + Optional + String + + + + + + Model Number + R + Single + Optional + String + + + + + + Serial Number + R + Single + Optional + String + + + + + + Firmware Version + R + Single + Optional + String + + + + + + Reboot + E + Single + Mandatory + + + + + + + Factory Reset + E + Single + Optional + + + + + + + Available Power Sources + R + Multiple + Optional + Integer + 0-7 + + + + + Power Source Voltage + R + Multiple + Optional + Integer + + mV + + + + Power Source Current + R + Multiple + Optional + Integer + + mA + + + + Battery Level + R + Single + Optional + Integer + 0-100 + % + + + + Memory Free + R + Single + Optional + Integer + + KB + + + + Error Code + R + Multiple + Mandatory + Integer + 0-8 + + + + + Reset Error Code + E + Single + Optional + + + + + + + Current Time + RW + Single + Optional + Time + + + + + + UTC Offset + RW + Single + Optional + String + + + + + + Timezone + RW + Single + Optional + String + + + + + + Supported Binding and Modes + R + Single + Mandatory + String + + + + + Device Type + R + Single + Optional + String + + + + + Hardware Version + R + Single + Optional + String + + + + + Software Version + R + Single + Optional + String + + + + + Battery Status + R + Single + Optional + Integer + 0-6 + + + + Memory Total + R + Single + Optional + Integer + + + + + ExtDevInfo + R + Multiple + Optional + Objlnk + + + + + + + diff --git a/application/src/test/resources/lwm2m/3-1_1.xml b/application/src/test/resources/lwm2m/3-1_1.xml new file mode 100644 index 0000000000..01151a57b7 --- /dev/null +++ b/application/src/test/resources/lwm2m/3-1_1.xml @@ -0,0 +1,331 @@ + + + + + + + Device + + 3 + urn:oma:lwm2m:oma:3:1.1 + 1.1 + 1.1 + Single + Mandatory + + + Manufacturer + R + Single + Optional + String + + + + + + Model Number + R + Single + Optional + String + + + + + + Serial Number + R + Single + Optional + String + + + + + + Firmware Version + R + Single + Optional + String + + + + + + Reboot + E + Single + Mandatory + + + + + + + Factory Reset + E + Single + Optional + + + + + + + Available Power Sources + R + Multiple + Optional + Integer + 0..7 + + + + + Power Source Voltage + R + Multiple + Optional + Integer + + + + + + Power Source Current + R + Multiple + Optional + Integer + + + + + + Battery Level + R + Single + Optional + Integer + 0..100 + % + + + + Memory Free + R + Single + Optional + Integer + + + + + + Error Code + R + Multiple + Mandatory + Integer + 0..8 + + + + + Reset Error Code + E + Single + Optional + + + + + + + Current Time + RW + Single + Optional + Time + + + + + + UTC Offset + RW + Single + Optional + String + + + + + + Timezone + RW + Single + Optional + String + + + + + + Supported Binding and Modes + R + Single + Mandatory + String + + + + + Device Type + R + Single + Optional + String + + + + + Hardware Version + R + Single + Optional + String + + + + + Software Version + R + Single + Optional + String + + + + + Battery Status + R + Single + Optional + Integer + 0..6 + + + + Memory Total + R + Single + Optional + Integer + + + + + ExtDevInfo + R + Multiple + Optional + Objlnk + + + + + + + diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 713d5b9fa8..3146eba95b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -24,6 +24,9 @@ import org.eclipse.leshan.core.LwM2m; import org.eclipse.leshan.core.LwM2m.Version; import org.eclipse.leshan.core.link.Link; import org.eclipse.leshan.core.link.attributes.Attribute; +import org.eclipse.leshan.core.link.lwm2m.MixedLwM2mLink; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; +import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mMultipleResource; @@ -453,22 +456,33 @@ public class LwM2mClient { } private void setSupportedClientObjects(){ - this.supportedClientObjects = new ConcurrentHashMap<>(); - for (Link link: this.registration.getSortedObjectLinks()) { - LwM2mPath lwM2mPath = new LwM2mPath(link.getUriReference()); - if (lwM2mPath.isObject()) { - LwM2m.Version ver; - if (link.getAttributes().get("ver")!= null) { - ver = new LwM2m.Version(link.getAttributes().get("ver").getValue().toString()); - } else { - ver = getDefaultObjectIDVer(); + if (this.registration.getSupportedObject() != null && this.registration.getSupportedObject().size() > 0) { + this.supportedClientObjects = this.registration.getSupportedObject(); + } else { + this.supportedClientObjects = new ConcurrentHashMap<>(); + for (Link link : this.registration.getSortedObjectLinks()) { + if (link instanceof MixedLwM2mLink) { + LwM2mPath path = ((MixedLwM2mLink) link).getPath(); + // add supported objects + if (path.isObject() || path.isObjectInstance()) { + int objectId = path.getObjectId(); + LwM2mAttribute versionParamValue = link.getAttributes().get(LwM2mAttributes.OBJECT_VERSION); + if (versionParamValue != null) { + // if there is a version attribute then use it as version for this object + this.supportedClientObjects.put(objectId, versionParamValue.getValue()); + } else { + // there is no version attribute attached. + // In this case we use the DEFAULT_VERSION only if this object stored as supported object. + Version currentVersion = this.supportedClientObjects.get(objectId); + if (currentVersion == null) { + this.supportedClientObjects.put(objectId, getDefaultObjectIDVer()); + } + } + } } - this.supportedClientObjects.put(lwM2mPath.getObjectId(), ver); - } else if (lwM2mPath.getObjectId() != null && this.supportedClientObjects.get(lwM2mPath.getObjectId()) == null){ - this.supportedClientObjects.put(lwM2mPath.getObjectId(), getDefaultObjectIDVer()); } } } - } + From 01e99efc4ed6e59c21c551ab0e5da11083d27fea Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 26 Feb 2025 12:28:01 +0200 Subject: [PATCH 17/19] Minor refactoring for TbContext --- .../server/actors/ruleChain/DefaultTbContext.java | 5 ++--- .../thingsboard/rule/engine/api/TbContext.java | 15 ++++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 091b97ac9d..24edd8f8bf 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.netty.channel.EventLoopGroup; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.Arrays; +import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.MailService; @@ -64,7 +65,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbActorMsg; @@ -193,8 +193,7 @@ public class DefaultTbContext implements TbContext { @Override public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) { - TopicPartitionInfo tpi = resolvePartition(tbMsg, MAIN_QUEUE_NAME); - enqueue(tpi, tbMsg, onFailure, onSuccess); + enqueue(tbMsg, MAIN_QUEUE_NAME, onSuccess, onFailure); } @Override diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index b517bb0051..b3fc646b94 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -144,11 +144,19 @@ public interface TbContext { /** * Puts new message to queue for processing by the Root Rule Chain + * WARNING: message is put to the Main queue. To specify other queue name - use {@link #enqueue(TbMsg, String, Runnable, Consumer)} * * @param msg - message */ void enqueue(TbMsg msg, Runnable onSuccess, Consumer onFailure); + /** + * Puts new message to custom queue for processing + * + * @param msg - message + */ + void enqueue(TbMsg msg, String queueName, Runnable onSuccess, Consumer onFailure); + /** * Sends message to the nested rule chain. * Fails processing of the message if the nested rule chain is not found. @@ -167,13 +175,6 @@ public interface TbContext { */ void output(TbMsg msg, String relationType); - /** - * Puts new message to custom queue for processing - * - * @param msg - message - */ - void enqueue(TbMsg msg, String queueName, Runnable onSuccess, Consumer onFailure); - void enqueueForTellFailure(TbMsg msg, String failureMessage); void enqueueForTellFailure(TbMsg tbMsg, Throwable t); From 45db2518171dba1eb9bf8ea85ebd36dfe0a24466 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 26 Feb 2025 12:56:07 +0100 Subject: [PATCH 18/19] used queueName from TbMsg fro enqueue --- .../actors/ruleChain/DefaultTbContext.java | 3 +- .../actors/rule/DefaultTbContextTest.java | 76 +++++++++++++++++++ .../rule/engine/api/TbContext.java | 3 +- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index cd8521027a..a1e8434b6e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -124,7 +124,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; -import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_DELETED; import static org.thingsboard.server.common.data.msg.TbMsgType.ATTRIBUTES_UPDATED; import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED; @@ -196,7 +195,7 @@ public class DefaultTbContext implements TbContext { @Override public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) { - enqueue(tbMsg, MAIN_QUEUE_NAME, onSuccess, onFailure); + enqueue(tbMsg, tbMsg.getQueueName(), onSuccess, onFailure); } @Override diff --git a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java index ea900afd4e..54205327ca 100644 --- a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java @@ -131,11 +131,87 @@ class DefaultTbContextTest { defaultTbContext.input(msg, ruleChainId); // THEN + then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); + } + + @MethodSource + @ParameterizedTest + public void givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) { + // GIVEN + var tpi = resolve(queueName); + + given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi); + var clusterService = mock(TbClusterService.class); + given(mainCtxMock.getClusterService()).willReturn(clusterService); + var callbackMock = mock(TbMsgCallback.class); + given(callbackMock.isMsgValid()).willReturn(true); + var ruleNode = new RuleNode(RULE_NODE_ID); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TENANT_ID) + .queueName(queueName) + .copyMetaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_STRING) + .callback(callbackMock) + .build(); + + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugSettings(DebugSettings.failures()); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + + // WHEN + defaultTbContext.enqueue(msg, () -> {}, t -> {}); + + // THEN + then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); + } + @MethodSource + @ParameterizedTest + public void givenMsgAndQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi(String queueName) { + // GIVEN + var tpi = resolve(queueName); + + given(mainCtxMock.resolve(eq(ServiceType.TB_RULE_ENGINE), eq(queueName), eq(TENANT_ID), eq(TENANT_ID))).willReturn(tpi); + var clusterService = mock(TbClusterService.class); + given(mainCtxMock.getClusterService()).willReturn(clusterService); + var callbackMock = mock(TbMsgCallback.class); + given(callbackMock.isMsgValid()).willReturn(true); + var ruleNode = new RuleNode(RULE_NODE_ID); + + var msg = TbMsg.newMsg() + .type(TbMsgType.POST_TELEMETRY_REQUEST) + .originator(TENANT_ID) + .copyMetaData(TbMsgMetaData.EMPTY) + .data(TbMsg.EMPTY_STRING) + .callback(callbackMock) + .build(); + + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugSettings(DebugSettings.failures()); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + + // WHEN + defaultTbContext.enqueue(msg, queueName, () -> {}, t -> {}); + + // THEN then(clusterService).should().pushMsgToRuleEngine(eq(tpi), eq(msg.getId()), any(), any()); } private static Stream givenMsgWithQueueName_whenInput_thenVerifyEnqueueWithCorrectTpi() { + return testQueueNames(); + } + + private static Stream givenMsgWithQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi() { + return testQueueNames(); + } + + private static Stream givenMsgAndQueueName_whenEnqueue_thenVerifyEnqueueWithCorrectTpi() { + return testQueueNames(); + } + + private static Stream testQueueNames() { return Stream.of("Main", "Test", null); } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index d743867b84..46b63e0595 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -143,8 +143,7 @@ public interface TbContext { void tellFailure(TbMsg msg, Throwable th); /** - * Puts new message to queue for processing by the Root Rule Chain - * WARNING: message is put to the Main queue. To specify other queue name - use {@link #enqueue(TbMsg, String, Runnable, Consumer)} + * Puts new message to queue from TbMsg for processing by the Root Rule Chain * * @param msg - message */ From 2c710535115f0212b2664d01727695c1ef8fe787 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 26 Feb 2025 15:48:20 +0200 Subject: [PATCH 19/19] Fix double version increment when creating entity in some cases --- .../DeviceProfileControllerTest.java | 74 +++++++++++++------ .../server/dao/sql/JpaAbstractDao.java | 20 ++--- 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java index b549d2af2a..663f5b9cde 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; @@ -60,6 +61,7 @@ import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; @@ -79,6 +81,8 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Autowired private DeviceProfileDao deviceProfileDao; + static final String LWM2M_PROFILE_JSON = "{\"name\":\"lwm2m profile\",\"type\":\"DEFAULT\",\"image\":null,\"defaultQueueName\":null,\"transportType\":\"LWM2M\",\"provisionType\":\"DISABLED\",\"description\":\"\",\"profileData\":{\"configuration\":{\"type\":\"DEFAULT\"},\"transportConfiguration\":{\"observeAttr\":{\"observe\":[],\"attribute\":[],\"telemetry\":[\"/11_1.1/0/0\"],\"keyName\":{\"/11_1.1/0/0\":\"profileName\"},\"attributeLwm2m\":{}},\"bootstrap\":[{\"shortServerId\":123,\"bootstrapServerIs\":false,\"host\":\"0.0.0.0\",\"port\":5685,\"clientHoldOffTime\":1,\"serverPublicKey\":\"\",\"serverCertificate\":\"\",\"bootstrapServerAccountTimeout\":0,\"lifetime\":300,\"defaultMinPeriod\":1,\"notifIfDisabled\":true,\"binding\":\"U\",\"securityMode\":\"NO_SEC\"}],\"clientLwM2mSettings\":{\"clientOnlyObserveAfterConnect\":1,\"fwUpdateStrategy\":1,\"swUpdateStrategy\":1,\"powerMode\":\"DRX\",\"edrxCycle\":81000,\"psmActivityTimer\":10000,\"pagingTransmissionWindow\":10000,\"defaultObjectIDVer\":\"1.0\"},\"bootstrapServerUpdateEnable\":false,\"type\":\"LWM2M\"},\"alarms\":null,\"provisionConfiguration\":{\"type\":\"DISABLED\"}}}"; + static class Config { @Bean @Primary @@ -119,7 +123,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); Assert.assertNotNull(savedDeviceProfile.getId()); Assert.assertTrue(savedDeviceProfile.getCreatedTime() > 0); @@ -135,7 +139,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { ActionType.ADDED); savedDeviceProfile.setName("New device profile"); - doPost("/api/deviceProfile", savedDeviceProfile, DeviceProfile.class); + saveDeviceProfile(savedDeviceProfile); DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName()); @@ -162,7 +166,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testFindDeviceProfileById() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); Assert.assertNotNull(foundDeviceProfile); Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); @@ -171,7 +175,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void whenGetDeviceProfileById_thenPermissionsAreChecked() throws Exception { DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null); - deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + deviceProfile = saveDeviceProfile(deviceProfile); loginDifferentTenant(); @@ -183,7 +187,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testFindDeviceProfileInfoById() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); DeviceProfileInfo foundDeviceProfileInfo = doGet("/api/deviceProfileInfo/" + savedDeviceProfile.getId().getId().toString(), DeviceProfileInfo.class); Assert.assertNotNull(foundDeviceProfileInfo); Assert.assertEquals(savedDeviceProfile.getId(), foundDeviceProfileInfo.getId()); @@ -213,7 +217,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void whenGetDeviceProfileInfoById_thenPermissionsAreChecked() throws Exception { DeviceProfile deviceProfile = createDeviceProfile("Device profile 1", null); - deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + deviceProfile = saveDeviceProfile(deviceProfile); loginDifferentTenant(); doGet("/api/deviceProfileInfo/" + deviceProfile.getId()) @@ -235,7 +239,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testSetDefaultDeviceProfile() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile 1"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Mockito.reset(tbClusterService, auditLogService); @@ -328,7 +332,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testChangeDeviceProfileTypeNull() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Mockito.reset(tbClusterService, auditLogService); @@ -345,7 +349,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testChangeDeviceProfileTransportTypeWithExistingDevices() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Device device = new Device(); device.setName("Test device"); device.setType("default"); @@ -367,7 +371,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testDeleteDeviceProfileWithExistingDevice() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Device device = new Device(); device.setName("Test device"); @@ -419,7 +423,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { public void testSaveDeviceProfileWithFirmwareFromDifferentTenant() throws Exception { loginDifferentTenant(); DeviceProfile differentProfile = createDeviceProfile("Different profile"); - differentProfile = doPost("/api/deviceProfile", differentProfile, DeviceProfile.class); + differentProfile = saveDeviceProfile(differentProfile); SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest(); firmwareInfo.setDeviceProfileId(differentProfile.getId()); firmwareInfo.setType(FIRMWARE); @@ -441,7 +445,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { public void testSaveDeviceProfileWithSoftwareFromDifferentTenant() throws Exception { loginDifferentTenant(); DeviceProfile differentProfile = createDeviceProfile("Different profile"); - differentProfile = doPost("/api/deviceProfile", differentProfile, DeviceProfile.class); + differentProfile = saveDeviceProfile(differentProfile); SaveOtaPackageInfoRequest softwareInfo = new SaveOtaPackageInfoRequest(); softwareInfo.setDeviceProfileId(differentProfile.getId()); softwareInfo.setType(SOFTWARE); @@ -462,7 +466,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testDeleteDeviceProfile() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Mockito.reset(tbClusterService, auditLogService); @@ -495,7 +499,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { int cntEntity = 28; for (int i = 0; i < cntEntity; i++) { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile" + i); - deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); + deviceProfiles.add(saveDeviceProfile(deviceProfile)); } testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new DeviceProfile(), new DeviceProfile(), @@ -552,7 +556,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { for (int i = 0; i < 28; i++) { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile" + i); - deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class)); + deviceProfiles.add(saveDeviceProfile(deviceProfile)); } List loadedDeviceProfileInfos = new ArrayList<>(); @@ -961,7 +965,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { JsonTransportPayloadConfiguration jsonTransportPayloadConfiguration = new JsonTransportPayloadConfiguration(); MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(jsonTransportPayloadConfiguration, true); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); Assert.assertEquals(savedDeviceProfile.getTransportType(), DeviceTransportType.MQTT); Assert.assertTrue(savedDeviceProfile.getProfileData().getTransportConfiguration() instanceof MqttDeviceProfileTransportConfiguration); @@ -979,7 +983,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { "v1/devices/me/telemetry", "v1/devices/me/attributes", "v1/devices/me/subscribeattributes"); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); Assert.assertEquals(savedDeviceProfile.getTransportType(), DeviceTransportType.MQTT); Assert.assertTrue(savedDeviceProfile.getProfileData().getTransportConfiguration() instanceof MqttDeviceProfileTransportConfiguration); @@ -997,7 +1001,7 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = this.createProtoTransportPayloadConfiguration(schema, schema, null, null); MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = this.createMqttDeviceProfileTransportConfiguration(protoTransportPayloadConfiguration, false); DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", mqttDeviceProfileTransportConfiguration); - DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + DeviceProfile savedDeviceProfile = saveDeviceProfile(deviceProfile); Assert.assertNotNull(savedDeviceProfile); DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString(), DeviceProfile.class); Assert.assertEquals(savedDeviceProfile, foundDeviceProfile); @@ -1036,14 +1040,14 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { @Test public void testDeleteDeviceProfileWithDeleteRelationsOk() throws Exception { - DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelationsOk").getId(); + DeviceProfileId deviceProfileId = saveDeviceProfile("DeviceProfile for Test WithRelationsOk").getId(); testEntityDaoWithRelationsOk(savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId); } @Ignore @Test public void testDeleteDeviceProfileExceptionWithRelationsTransactional() throws Exception { - DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelations Transactional Exception").getId(); + DeviceProfileId deviceProfileId = saveDeviceProfile("DeviceProfile for Test WithRelations Transactional Exception").getId(); testEntityDaoWithRelationsTransactionalException(deviceProfileDao, savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId); } @@ -1103,8 +1107,36 @@ public class DeviceProfileControllerTest extends AbstractControllerTest { Assert.assertEquals(count, deviceProfileNames.size()); } - private DeviceProfile savedDeviceProfile(String name) { + @Test + public void testSaveDeviceProfileWithOutdatedVersion() throws Exception { + DeviceProfile deviceProfile = JacksonUtil.fromString(LWM2M_PROFILE_JSON, DeviceProfile.class); + deviceProfile.setName("Device profile v1.0"); + deviceProfile = saveDeviceProfile(deviceProfile); + assertThat(deviceProfile.getVersion()).isOne(); + + deviceProfile.setName("Device profile v2.0"); + deviceProfile = saveDeviceProfile(deviceProfile); + assertThat(deviceProfile.getVersion()).isEqualTo(2); + + deviceProfile.setName("Device profile v1.1"); + deviceProfile.setVersion(1L); + String response = doPost("/api/deviceProfile", deviceProfile).andExpect(status().isConflict()) + .andReturn().getResponse().getContentAsString(); + assertThat(JacksonUtil.toJsonNode(response).get("message").asText()) + .containsIgnoringCase("already changed by someone else"); + + deviceProfile.setVersion(null); // overriding entity + deviceProfile = saveDeviceProfile(deviceProfile); + assertThat(deviceProfile.getName()).isEqualTo("Device profile v1.1"); + assertThat(deviceProfile.getVersion()).isEqualTo(3); + } + + private DeviceProfile saveDeviceProfile(String name) { DeviceProfile deviceProfile = createDeviceProfile(name); + return saveDeviceProfile(deviceProfile); + } + + private DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile) { return doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java index f044629364..0163291aba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDao.java @@ -88,10 +88,10 @@ public abstract class JpaAbstractDao, D> boolean flushed = false; EntityManager entityManager = getEntityManager(); if (isNew) { + entityManager.persist(entity); if (entity instanceof HasVersion versionedEntity) { versionedEntity.setVersion(1L); } - entityManager.persist(entity); } else { if (entity instanceof HasVersion versionedEntity) { if (versionedEntity.getVersion() == null) { @@ -106,23 +106,25 @@ public abstract class JpaAbstractDao, D> } } versionedEntity = entityManager.merge(versionedEntity); + entity = (E) versionedEntity; /* * by default, Hibernate doesn't issue an update query and thus version increment * if the entity was not modified. to bypass this and always increment the version, we do it manually * */ versionedEntity.setVersion(versionedEntity.getVersion() + 1); - /* - * flushing and then removing the entity from the persistence context so that it is not affected - * by next flushes (e.g. when a transaction is committed) to avoid double version increment - * */ - entityManager.flush(); - entityManager.detach(versionedEntity); - flushed = true; - entity = (E) versionedEntity; } else { entity = entityManager.merge(entity); } } + if (entity instanceof HasVersion versionedEntity) { + /* + * flushing and then removing the entity from the persistence context so that it is not affected + * by next flushes (e.g. when a transaction is committed) to avoid double version increment + * */ + entityManager.flush(); + entityManager.detach(versionedEntity); + flushed = true; + } if (flush && !flushed) { entityManager.flush(); }