Browse Source

Merge remote-tracking branch 'thingsboard-ce/lts-4.3' into fix/power-button

pull/14959/head
Maksym Tsymbarov 8 months ago
parent
commit
819a045937
  1. 31
      TEST_FAST.md
  2. 3
      application/pom.xml
  3. 13
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 10
      application/src/main/java/org/thingsboard/server/actors/calculatedField/AbstractCalculatedFieldActor.java
  5. 60
      application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java
  6. 10
      application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java
  8. 3
      application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java
  9. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java
  10. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java
  11. 15
      application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java
  12. 10
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  13. 91
      application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java
  14. 11
      application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java
  15. 12
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  16. 9
      application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java
  17. 97
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  18. 14
      application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java
  19. 14
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  20. 10
      application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
  21. 41
      application/src/test/java/org/thingsboard/server/edge/EdgeLatestVersionTest.java
  22. 19
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  23. 28
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  24. 204
      application/src/test/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityServiceTest.java
  25. 5
      application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java
  26. 14
      application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java
  27. 3
      application/src/test/java/org/thingsboard/server/transport/coap/CoapTestClient.java
  28. 37
      application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java
  29. 14
      application/src/test/java/org/thingsboard/server/transport/coap/x509/CoapClientX509Test.java
  30. 70
      application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java
  31. 2
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java
  32. 41
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java
  33. 1
      application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java
  34. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java
  35. 4
      application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java
  36. 13
      application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java
  37. 10
      application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java
  38. 6
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java
  39. 6
      application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java
  40. 42
      application/src/test/java/org/thingsboard/server/utils/PortFinder.java
  41. 1
      application/src/test/resources/application-test.properties
  42. 6
      application/src/test/resources/logback-test.xml
  43. 2
      common/actor/pom.xml
  44. 2
      common/cache/pom.xml
  45. 3
      common/cache/src/test/resources/logback-test.xml
  46. 2
      common/cluster-api/pom.xml
  47. 2
      common/coap-server/pom.xml
  48. 2
      common/dao-api/pom.xml
  49. 2
      common/data/pom.xml
  50. 2
      common/discovery-api/pom.xml
  51. 7
      common/edge-api/pom.xml
  52. 11
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  53. 86
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeVersionComparator.java
  54. 2
      common/edge-api/src/main/proto/edge.proto
  55. 102
      common/edge-api/src/test/java/org/thingsboard/edge/rpc/EdgeVersionComparatorTest.java
  56. 2
      common/edqs/pom.xml
  57. 3
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbRocksDb.java
  58. 2
      common/message/pom.xml
  59. 2
      common/pom.xml
  60. 2
      common/proto/pom.xml
  61. 2
      common/queue/pom.xml
  62. 2
      common/script/pom.xml
  63. 2
      common/script/remote-js-client/pom.xml
  64. 2
      common/script/script-api/pom.xml
  65. 2
      common/stats/pom.xml
  66. 2
      common/transport/coap/pom.xml
  67. 2
      common/transport/http/pom.xml
  68. 2
      common/transport/lwm2m/pom.xml
  69. 2
      common/transport/mqtt/pom.xml
  70. 2
      common/transport/pom.xml
  71. 2
      common/transport/snmp/pom.xml
  72. 2
      common/transport/transport-api/pom.xml
  73. 2
      common/util/pom.xml
  74. 2
      common/version-control/pom.xml
  75. 2
      dao/pom.xml
  76. 7
      dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java
  77. 16
      dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java
  78. 16
      dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java
  79. 154
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java
  80. 1
      dao/src/test/resources/application-test.properties
  81. 3
      dao/src/test/resources/logback-test.xml
  82. 2
      edqs/pom.xml
  83. 2
      monitoring/pom.xml
  84. 2
      msa/black-box-tests/pom.xml
  85. 4
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractCoapClientTest.java
  86. 42
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/PortFinder.java
  87. 24
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java
  88. 8
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/AbstractLwm2mClientTest.java
  89. 21
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/LwM2MTestClient.java
  90. 3
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/rule/node/MqttNodeTest.java
  91. 2
      msa/edqs/pom.xml
  92. 2
      msa/js-executor/package.json
  93. 2
      msa/js-executor/pom.xml
  94. 2
      msa/monitoring/pom.xml
  95. 2
      msa/pom.xml
  96. 2
      msa/tb-node/pom.xml
  97. 2
      msa/tb/pom.xml
  98. 2
      msa/transport/coap/pom.xml
  99. 2
      msa/transport/http/pom.xml
  100. 2
      msa/transport/lwm2m/pom.xml

31
TEST_FAST.md

@ -0,0 +1,31 @@
## Running tests in parallel with a reasonable memory usage
```bash
export MAVEN_OPTS="-Xmx1024m"
export NODE_OPTIONS="--max_old_space_size=4096"
export SUREFIRE_JAVA_OPTS="-Xmx1200m -Xss256k -XX:+ExitOnOutOfMemoryError"
mvn clean install -T6 -DskipTests
mvn test -pl='!application,!dao,!ui-ngx,!msa/js-executor,!msa/web-ui' -T4
mvn test -pl dao -Dparallel=packages -DforkCount=4
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.controller.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.edge.**' -DforkCount=4 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.service.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.mqtt.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.coap.**' -DforkCount=6 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='org.thingsboard.server.transport.lwm2m.**' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
mvn test -pl application -Dsurefire.excludes='**/nosql/*Test.java' -Dtest='**/*TestSuite.java' -DforkCount=4 -Dparallel=classes -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
#the rest of application tests
mvn test -pl application -Dtest='
!**/nosql/*Test.java,
!org.thingsboard.server.controller.**,
!org.thingsboard.server.edge.**,
!org.thingsboard.server.service.**,
!org.thingsboard.server.transport.mqtt.**,
!org.thingsboard.server.transport.coap.**,
!org.thingsboard.server.transport.lwm2m.**
' -DforkCount=6 -Dparallel=packages -Dsurefire.rerunFailingTestsCount=2 -Dsurefire.failOnFlakeCount=5
```

3
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>application</artifactId>
@ -458,6 +458,7 @@
<configuration>
<systemPropertyVariables>
<spring.config.name>thingsboard</spring.config.name>
<project.version>${project.version}</project.version>
</systemPropertyVariables>
<excludes>
<exclude>**/nosql/*Test.java</exclude>

13
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -42,6 +42,7 @@ import org.thingsboard.rule.engine.api.notification.SlackService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.calculatedField.CalculatedFieldException;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.cache.limits.RateLimitService;
@ -831,6 +832,18 @@ public class ActorSystemContext {
Futures.addCallback(future, RULE_CHAIN_DEBUG_EVENT_ERROR_CALLBACK, MoreExecutors.directExecutor());
}
public void persistCalculatedFieldDebugError(CalculatedFieldException cfe) {
String message;
if (cfe.getErrorMessage() != null) {
message = cfe.getErrorMessage();
} else if (cfe.getCause() != null) {
message = cfe.getCause().getMessage();
} else {
message = "N/A";
}
persistCalculatedFieldDebugEvent(cfe.getCtx().getTenantId(), cfe.getCtx().getCfId(), cfe.getEventEntity(), cfe.getArguments(), cfe.getMsgId(), cfe.getMsgType(), null, message);
}
public void persistCalculatedFieldDebugEvent(TenantId tenantId, CalculatedFieldId calculatedFieldId, EntityId entityId, JsonNode arguments, UUID tbMsgId, String tbMsgType, String result, String errorMessage) {
if (checkLimits(tenantId)) {
try {

10
application/src/main/java/org/thingsboard/server/actors/calculatedField/AbstractCalculatedFieldActor.java

@ -41,15 +41,7 @@ public abstract class AbstractCalculatedFieldActor extends ContextAwareActor {
return doProcessCfMsg(cfm);
} catch (CalculatedFieldException cfe) {
if (DebugModeUtil.isDebugFailuresAvailable(cfe.getCtx().getCalculatedField())) {
String message;
if (cfe.getErrorMessage() != null) {
message = cfe.getErrorMessage();
} else if (cfe.getCause() != null) {
message = cfe.getCause().getMessage();
} else {
message = "N/A";
}
systemContext.persistCalculatedFieldDebugEvent(tenantId, cfe.getCtx().getCfId(), cfe.getEventEntity(), cfe.getArguments(), cfe.getMsgId(), cfe.getMsgType(), null, message);
systemContext.persistCalculatedFieldDebugError(cfe);
}
cause = cfe.getCause();
} catch (Exception e) {

60
application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

@ -171,7 +171,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
if (state.isSizeOk()) {
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, msg.getEventType().name(), msg.getCallback());
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build();
}
} else {
msg.getCallback().onSuccess();
@ -261,7 +261,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build();
}
} catch (Exception e) {
log.debug("[{}][{}] Failed to initialize CF state", entityId, ctx.getCfId(), e);
log.debug("[{}][{}] Failed to handle relation update", entityId, ctx.getCfId(), e);
if (e instanceof CalculatedFieldException cfe) {
throw cfe;
}
@ -273,31 +273,39 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
CalculatedFieldCtx ctx = msg.getCalculatedField();
CalculatedFieldId cfId = ctx.getCfId();
CalculatedFieldState state = states.get(cfId);
if (state == null) {
msg.getCallback().onSuccess();
return;
}
if (state instanceof RelatedEntitiesAggregationCalculatedFieldState aggState) {
aggState.cleanupEntityData(msg.getRelatedEntityId());
try {
if (state == null) {
msg.getCallback().onSuccess();
return;
}
if (state instanceof RelatedEntitiesAggregationCalculatedFieldState aggState) {
aggState.cleanupEntityData(msg.getRelatedEntityId());
state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize());
state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize());
if (state.isSizeOk()) {
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, TbMsgType.RELATION_DELETED.name(), msg.getCallback());
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
if (state.isSizeOk()) {
processStateIfReady(state, Collections.emptyMap(), ctx, Collections.singletonList(ctx.getCfId()), null, TbMsgType.RELATION_DELETED.name(), msg.getCallback());
} else {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build();
}
return;
}
if (state instanceof PropagationCalculatedFieldState propagationState) {
PropagationArgumentEntry entry = new PropagationArgumentEntry();
entry.setRemoved(msg.getRelatedEntityId());
propagationState.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, entry), ctx);
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArgumentsJson(), null, TbMsgType.RELATION_DELETED.name(), null, null);
}
}
return;
}
if (state instanceof PropagationCalculatedFieldState propagationState) {
PropagationArgumentEntry entry = new PropagationArgumentEntry();
entry.setRemoved(msg.getRelatedEntityId());
propagationState.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, entry), ctx);
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArgumentsJson(), null, TbMsgType.RELATION_DELETED.name(), null, null);
msg.getCallback().onSuccess();
} catch (Exception e) {
log.debug("[{}][{}] Failed to handle relation delete", entityId, ctx.getCfId(), e);
if (e instanceof CalculatedFieldException cfe) {
throw cfe;
}
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
}
msg.getCallback().onSuccess();
}
public void process(EntityCalculatedFieldTelemetryMsg msg) throws CalculatedFieldException {
@ -366,7 +374,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
} catch (Exception e) {
log.debug("[{}][{}] Failed to process CF telemetry msg: {}", entityId, ctx.getCfId(), proto, e);
if (e instanceof CalculatedFieldException cfe) {
throw cfe;
if (DebugModeUtil.isDebugFailuresAvailable(cfe.getCtx().getCalculatedField())) {
systemContext.persistCalculatedFieldDebugError(cfe);
}
callback.onSuccess();
return;
}
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build();
}
@ -384,7 +396,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
log.debug("[{}][{}] Reevaluating CF state", entityId, cfId);
processStateIfReady(state, null, ctx, Collections.singletonList(cfId), null, CalculatedFieldEventType.REEVALUATION_MSG.name(), msg.getCallback());
} else {
throw new RuntimeException(ctx.getSizeExceedsLimitMessage());
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build();
}
}

10
application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java

@ -77,7 +77,11 @@ public class ApiKeyController extends BaseController {
User user = checkUserId(apiKeyInfo.getUserId(), Operation.WRITE);
apiKeyInfo.setTenantId(user.getTenantId());
checkEntity(apiKeyInfo.getId(), apiKeyInfo, Resource.API_KEY);
return checkNotNull(apiKeyService.saveApiKey(apiKeyInfo.getTenantId(), apiKeyInfo));
ApiKey savedApiKey = checkNotNull(apiKeyService.saveApiKey(apiKeyInfo.getTenantId(), apiKeyInfo));
if (apiKeyInfo.getId() != null) {
savedApiKey.setValue(null);
}
return savedApiKey;
}
@ApiOperation(value = "Get User Api Keys (getUserApiKeys)",
@ -121,7 +125,7 @@ public class ApiKeyController extends BaseController {
ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.WRITE);
checkUserId(apiKey.getUserId(), Operation.WRITE);
apiKey.setDescription(description.orElse(null));
return apiKeyService.saveApiKey(apiKey.getTenantId(), apiKey);
return new ApiKeyInfo(apiKeyService.saveApiKey(apiKey.getTenantId(), apiKey));
}
@ApiOperation(value = "Enable or disable API key (enableApiKey)",
@ -137,7 +141,7 @@ public class ApiKeyController extends BaseController {
ApiKey apiKey = checkApiKeyId(apiKeyId, Operation.WRITE);
checkUserId(apiKey.getUserId(), Operation.WRITE);
apiKey.setEnabled(enabledValue);
return apiKeyService.saveApiKey(apiKey.getTenantId(), apiKey);
return new ApiKeyInfo(apiKeyService.saveApiKey(apiKey.getTenantId(), apiKey));
}
@ApiOperation(value = "Delete API key by ID (deleteApiKey)",

2
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java

@ -804,7 +804,7 @@ public class CalculatedFieldCtx implements Closeable {
}
public String getSizeExceedsLimitMessage() {
return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!";
return "State size exceeds limit of " + (maxStateSize / 1024) + "Kb!";
}
public boolean hasCurrentOwnerSourceArguments() {

3
application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java

@ -26,6 +26,7 @@ import com.google.gson.JsonPrimitive;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.edge.rpc.EdgeVersionComparator;
import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode;
import org.thingsboard.rule.engine.ai.TbAiNode;
import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode;
@ -281,7 +282,7 @@ public class EdgeMsgConstructorUtils {
public static String getEntityAndFixLwm2mBootstrapShortServerId(DeviceProfile deviceProfile, EdgeVersion edgeVersion) {
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
if (!(transportConfiguration instanceof Lwm2mDeviceProfileTransportConfiguration) || edgeVersion.getNumber() >= EdgeVersion.V_4_3_0.getNumber()) {
if (!(transportConfiguration instanceof Lwm2mDeviceProfileTransportConfiguration) || EdgeVersionComparator.INSTANCE.compare(edgeVersion, EdgeVersion.V_4_3_0) >= 0) {
return JacksonUtil.toString(deviceProfile);
}
JsonNode jsonNode = JacksonUtil.valueToTree(deviceProfile);

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java

@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.edge.rpc.EdgeVersionComparator;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.cf.CalculatedField;
@ -116,7 +117,7 @@ public class CalculatedFieldEdgeProcessor extends BaseCalculatedFieldProcessor i
}
private boolean isValidCfToSend(CalculatedFieldType type, EdgeVersion edgeVersion) {
return edgeVersion.getNumber() >= EdgeVersion.V_4_3_0.getNumber() || (type == SIMPLE || type == SCRIPT);
return EdgeVersionComparator.INSTANCE.compare(edgeVersion, EdgeVersion.V_4_3_0) >= 0 || (type == SIMPLE || type == SCRIPT);
}
@Override

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java

@ -16,13 +16,14 @@
package org.thingsboard.server.service.edge.rpc.utils;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.edge.rpc.EdgeVersionComparator;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@Slf4j
public final class EdgeVersionUtils {
public static boolean isEdgeVersionOlderThan(EdgeVersion currentVersion, EdgeVersion requiredVersion) {
return currentVersion.ordinal() < requiredVersion.ordinal();
return EdgeVersionComparator.INSTANCE.compare(currentVersion, requiredVersion) < 0;
}
}

15
application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java

@ -21,17 +21,18 @@ import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.thingsboard.server.service.install.update.DefaultDataUpdateService;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
@RequiredArgsConstructor
public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSettingsService {
// This list should include all versions that are compatible for the upgrade in 4 digits format (like 4.2.0.0, etc.).
// The compatibility cycle usually breaks when we have some scripts written in Java that may not work after a new release.
// TODO: don't check the "patch" number, since upgrade is not required for patch releases
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.1.0", "4.2.1.1", "4.2.1.2");
// map of versions from which the upgrade to the current version is possible
// key - supported version prefix, value - display name
private static final Map<String, String> SUPPORTED_VERSIONS_FOR_UPGRADE = Map.of(
"4.2.1", "4.2.1.x"
);
private final ProjectInfo projectInfo;
private final JdbcTemplate jdbcTemplate;
@ -56,9 +57,9 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
onSchemaSettingsError("Upgrade failed: database already upgraded to current version. You can set SKIP_SCHEMA_VERSION_CHECK to 'true' if force re-upgrade needed.");
}
if (!SUPPORTED_VERSIONS_FOR_UPGRADE.contains(dbSchemaVersion)) {
if (SUPPORTED_VERSIONS_FOR_UPGRADE.keySet().stream().noneMatch(dbSchemaVersion::startsWith)) {
onSchemaSettingsError(String.format("Upgrade failed: database version '%s' is not supported for upgrade. Supported versions are: %s.",
dbSchemaVersion, SUPPORTED_VERSIONS_FOR_UPGRADE
dbSchemaVersion, SUPPORTED_VERSIONS_FOR_UPGRADE.values()
));
}
}

10
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -80,11 +80,15 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
@Override
public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException {
if (!userCredentials.isEnabled()) {
throw new DisabledException("User is not active");
}
if (!encoder.matches(password, userCredentials.getPassword())) {
int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();
if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) {
if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) {
if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts()) {
lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
throw new LockedException("Authentication Failed. Username was locked due to security policy.");
}
@ -92,10 +96,6 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
}
if (!userCredentials.isEnabled()) {
throw new DisabledException("User is not active");
}
userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = securitySettingsService.getSecuritySettings();

91
application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java

@ -20,10 +20,12 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
@ -45,7 +47,9 @@ import org.thingsboard.server.common.data.cf.configuration.geofencing.Geofencing
import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationPathLevel;
@ -1390,6 +1394,93 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
});
}
@Test
public void testCalculatedFieldsWhenOneIsInvalid() throws Exception {
Device testDevice = createDevice("Test device", "1234567890");
long now = System.currentTimeMillis();
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + AttributeScope.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":5}}", now - TimeUnit.MINUTES.toMillis(3))));
// Script CF - invalid
CalculatedField invalidCF = new CalculatedField();
invalidCF.setEntityId(testDevice.getId());
invalidCF.setType(CalculatedFieldType.SCRIPT);
invalidCF.setName("Script CF");
invalidCF.setDebugSettings(DebugSettings.all());
ScriptCalculatedFieldConfiguration scriptConfig = new ScriptCalculatedFieldConfiguration();
ReferencedEntityKey refEntityKeyA = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null);
Argument argumentA = new Argument();
argumentA.setRefEntityKey(refEntityKeyA);
scriptConfig.setArguments(Map.of("a", argumentA));
scriptConfig.setExpression("""
return {
"temperature": temp
};
""");
scriptConfig.setOutput(new TimeSeriesOutput());
invalidCF.setConfiguration(scriptConfig);
invalidCF = doPost("/api/calculatedField", invalidCF, CalculatedField.class);
CalculatedFieldId invalidCfId = invalidCF.getId();
await().alias("create invalid CF -> check error").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
PageData<EventInfo> debugEvents = getDebugEvents(tenantId, invalidCfId, 1);
if (!debugEvents.getData().isEmpty()) {
EventInfo eventInfo = debugEvents.getData().get(0);
assertThat(eventInfo.getBody().has("error")).isTrue();
}
});
// Simple CF - valid
CalculatedField validCF = new CalculatedField();
validCF.setEntityId(testDevice.getId());
validCF.setType(CalculatedFieldType.SIMPLE);
validCF.setName("Simple CF");
validCF.setDebugSettings(DebugSettings.all());
SimpleCalculatedFieldConfiguration simpleConfig = new SimpleCalculatedFieldConfiguration();
simpleConfig.setArguments(Map.of("a", argumentA));
simpleConfig.setExpression("a+1");
TimeSeriesOutput simpleOutput = new TimeSeriesOutput();
simpleOutput.setName("a+1");
simpleOutput.setDecimalsByDefault(0);
simpleConfig.setOutput(simpleOutput);
validCF.setConfiguration(simpleConfig);
validCF = doPost("/api/calculatedField", validCF, CalculatedField.class);
CalculatedFieldId validCfId = validCF.getId();
await().alias("create CF -> check initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
PageData<EventInfo> debugEvents = getDebugEvents(tenantId, validCfId, 1);
if (!debugEvents.getData().isEmpty()) {
EventInfo eventInfo = debugEvents.getData().get(0);
assertThat(eventInfo.getBody().has("error")).isFalse();
}
ObjectNode result = getLatestTelemetry(testDevice.getId(), "a+1");
assertThat(result).isNotNull();
assertThat(result.get("a+1").get(0).get("value").asText()).isEqualTo("6");
});
doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + AttributeScope.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"a\":6}"));
await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
ObjectNode result = getLatestTelemetry(testDevice.getId(), "a+1");
assertThat(result).isNotNull();
assertThat(result.get("a+1").get(0).get("value").asText()).isEqualTo("7");
});
}
private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception {
return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class);
}

11
application/src/test/java/org/thingsboard/server/cf/EntityAggregationCalculatedFieldTest.java

@ -205,6 +205,15 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
CustomInterval customInterval = new CustomInterval(TZ, 0L, 2L);
createConsumptionCF(device.getId(), customInterval, null);
long interval = customInterval.getCurrentIntervalDurationMillis();
// Wait for a fresh interval
long initialIntervalStart = customInterval.getCurrentIntervalStartTs();
await().alias("wait for fresh interval")
.atMost(interval + 100, TimeUnit.MILLISECONDS)
.pollInterval(100, TimeUnit.MILLISECONDS)
.until(() -> customInterval.getCurrentIntervalStartTs() != initialIntervalStart);
long currentIntervalStartTs = customInterval.getCurrentIntervalStartTs();
long tsBeforeInterval = currentIntervalStartTs - 1000;
@ -216,8 +225,6 @@ public class EntityAggregationCalculatedFieldTest extends AbstractControllerTest
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":180}}", tsInInterval_2));
postTelemetry(device.getId(), String.format("{\"ts\": \"%s\", \"values\": {\"energy\":120}}", tsInInterval_3));
long interval = customInterval.getCurrentIntervalDurationMillis();
await().alias("create CF -> perform aggregation after interval end")
.atMost(2 * interval, TimeUnit.MILLISECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)

12
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -60,6 +60,7 @@ import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext;
@ -349,7 +350,14 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
if (this.mockMvc == null) {
this.mockMvc = webAppContextSetup(webApplicationContext)
.apply(springSecurity()).build();
.apply(springSecurity())
// conditional printing of non 2xx responses
.alwaysDo(result -> {
if (result.getResponse().getStatus() >= 400) {
MockMvcResultHandlers.log().handle(result);
}
})
.build();
}
loginSysAdmin();
@ -423,7 +431,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
jdbcTemplate.execute("TRUNCATE TABLE notification");
log.info("Executed web test teardown");
log.debug("Executed web test teardown");
}
private void verifyNoTenantsLeft() throws Exception {

9
application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java

@ -53,7 +53,14 @@ public class ApiKeyControllerTest extends AbstractControllerTest {
Assert.assertEquals(tenantId, savedApiKey.getTenantId());
Assert.assertEquals(tenantAdminUser.getId(), savedApiKey.getUserId());
doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk());
String newDescription = "Updated API Key Description";
savedApiKey.setDescription(newDescription);
ApiKey updatedApiKey = doPost("/api/apiKey", savedApiKey, ApiKey.class);
Assert.assertNotNull(updatedApiKey);
Assert.assertEquals(newDescription, updatedApiKey.getDescription());
Assert.assertNull("Verify we do not expose API key value on update", updatedApiKey.getValue());
doDelete("/api/apiKey/" + updatedApiKey.getId()).andExpect(status().isOk());
}
@Test

97
application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java

@ -36,7 +36,10 @@ 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.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.TestSocketUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.AdminSettings;
@ -99,6 +102,7 @@ import org.thingsboard.server.service.edge.instructions.EdgeUpgradeInstructionsS
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -121,7 +125,12 @@ import static org.thingsboard.server.edge.AbstractEdgeTest.CONNECT_MESSAGE_COUNT
public class EdgeControllerTest extends AbstractControllerTest {
public static final String EDGE_HOST = "localhost";
public static final int EDGE_PORT = 7070;
public static final int EDGE_PORT = TestSocketUtils.findAvailableTcpPort();
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
log.debug("edges.rpc.port = {}", EDGE_PORT);
registry.add("edges.rpc.port", () -> EDGE_PORT);
}
private IdComparator<Edge> idComparator = new IdComparator<>();
@ -916,11 +925,11 @@ public class EdgeControllerTest extends AbstractControllerTest {
verifyFetchersMsgs(edgeImitator, savedDevice);
// verify queue msgs
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1"));
Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId()));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1"));
Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgsDeque(), savedDevice.getId()));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1"));
printQueueMsgsIfNotEmpty(edgeImitator);
// 17 connect messages
@ -1000,32 +1009,32 @@ public class EdgeControllerTest extends AbstractControllerTest {
}
private void verifyFetchersMsgs(EdgeImitator edgeImitator, Device savedDevice) {
Assert.assertTrue(popQueueMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Main"));
Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Edge Root Rule Chain"));
Assert.assertTrue(popRuleChainMetadataMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, getEdgeRootRuleChainId(edgeImitator)));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "general"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "connectivity"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "jwt"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popUserCredentialsMsg(edgeImitator.getDownlinkMsgs(), currentUserId));
Assert.assertTrue(popUserMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, TENANT_ADMIN_EMAIL, Authority.TENANT_ADMIN));
Assert.assertTrue(popCustomerMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Public"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1"));
Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId()));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1"));
Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgs(), tenantId));
Assert.assertTrue(popTenantProfileMsg(edgeImitator.getDownlinkMsgs(), tenantProfileId));
Assert.assertTrue(popSyncCompletedMsg(edgeImitator.getDownlinkMsgs()));
Assert.assertTrue(popQueueMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Main"));
Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Edge Root Rule Chain"));
Assert.assertTrue(popRuleChainMetadataMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, getEdgeRootRuleChainId(edgeImitator)));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgsDeque(), "general"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgsDeque(), "mail"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgsDeque(), "connectivity"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgsDeque(), "jwt"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popUserCredentialsMsg(edgeImitator.getDownlinkMsgsDeque(), currentUserId));
Assert.assertTrue(popUserMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, TENANT_ADMIN_EMAIL, Authority.TENANT_ADMIN));
Assert.assertTrue(popCustomerMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Public"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1"));
Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgsDeque(), savedDevice.getId()));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgsDeque(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1"));
Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgsDeque(), tenantId));
Assert.assertTrue(popTenantProfileMsg(edgeImitator.getDownlinkMsgsDeque(), tenantProfileId));
Assert.assertTrue(popSyncCompletedMsg(edgeImitator.getDownlinkMsgsDeque()));
}
private boolean popQueueMsg(List<AbstractMessage> messages, UpdateMsgType msgType, String name) {
private boolean popQueueMsg(Deque<AbstractMessage> messages, UpdateMsgType msgType, String name) {
for (AbstractMessage message : messages) {
if (message instanceof QueueUpdateMsg queueUpdateMsg) {
Queue queue = JacksonUtil.fromString(queueUpdateMsg.getEntity(), Queue.class, true);
@ -1039,7 +1048,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popRuleChainMsg(List<AbstractMessage> messages, UpdateMsgType msgType, String name) {
private boolean popRuleChainMsg(Deque<AbstractMessage> messages, UpdateMsgType msgType, String name) {
for (AbstractMessage message : messages) {
if (message instanceof RuleChainUpdateMsg ruleChainUpdateMsg) {
RuleChain ruleChain = JacksonUtil.fromString(ruleChainUpdateMsg.getEntity(), RuleChain.class, true);
@ -1055,7 +1064,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popRuleChainMetadataMsg(List<AbstractMessage> messages, UpdateMsgType msgType, RuleChainId ruleChainId) {
private boolean popRuleChainMetadataMsg(Deque<AbstractMessage> messages, UpdateMsgType msgType, RuleChainId ruleChainId) {
for (AbstractMessage message : messages) {
if (message instanceof RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) {
RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true);
@ -1070,7 +1079,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popAdminSettingsMsg(List<AbstractMessage> messages, String key) {
private boolean popAdminSettingsMsg(Deque<AbstractMessage> messages, String key) {
for (AbstractMessage message : messages) {
if (message instanceof AdminSettingsUpdateMsg adminSettingsUpdateMsg) {
AdminSettings adminSettings = JacksonUtil.fromString(adminSettingsUpdateMsg.getEntity(), AdminSettings.class, true);
@ -1084,7 +1093,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popDeviceProfileMsg(List<AbstractMessage> messages, UpdateMsgType msgType, String name) {
private boolean popDeviceProfileMsg(Deque<AbstractMessage> messages, UpdateMsgType msgType, String name) {
for (AbstractMessage message : messages) {
if (message instanceof DeviceProfileUpdateMsg deviceProfileUpdateMsg) {
DeviceProfile deviceProfile = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true);
@ -1099,7 +1108,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popDeviceMsg(List<AbstractMessage> messages, UpdateMsgType msgType, String name) {
private boolean popDeviceMsg(Deque<AbstractMessage> messages, UpdateMsgType msgType, String name) {
for (AbstractMessage message : messages) {
if (message instanceof DeviceUpdateMsg deviceUpdateMsg) {
Device device = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true);
@ -1114,7 +1123,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popDeviceCredentialsMsg(List<AbstractMessage> messages, DeviceId deviceId) {
private boolean popDeviceCredentialsMsg(Deque<AbstractMessage> messages, DeviceId deviceId) {
for (AbstractMessage message : messages) {
if (message instanceof DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) {
DeviceCredentials deviceCredentials = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true);
@ -1128,7 +1137,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popAssetProfileMsg(List<AbstractMessage> messages, UpdateMsgType msgType, String name) {
private boolean popAssetProfileMsg(Deque<AbstractMessage> messages, UpdateMsgType msgType, String name) {
for (AbstractMessage message : messages) {
if (message instanceof AssetProfileUpdateMsg assetProfileUpdateMsg) {
AssetProfile assetProfile = JacksonUtil.fromString(assetProfileUpdateMsg.getEntity(), AssetProfile.class, true);
@ -1143,7 +1152,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popAssetMsg(List<AbstractMessage> messages, UpdateMsgType msgType, String name) {
private boolean popAssetMsg(Deque<AbstractMessage> messages, UpdateMsgType msgType, String name) {
for (AbstractMessage message : messages) {
if (message instanceof AssetUpdateMsg assetUpdateMsg) {
Asset asset = JacksonUtil.fromString(assetUpdateMsg.getEntity(), Asset.class, true);
@ -1158,7 +1167,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popUserCredentialsMsg(List<AbstractMessage> messages, UserId userId) {
private boolean popUserCredentialsMsg(Deque<AbstractMessage> messages, UserId userId) {
for (AbstractMessage message : messages) {
if (message instanceof UserCredentialsUpdateMsg userCredentialsUpdateMsg) {
UserCredentials userCredentials = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);
@ -1172,7 +1181,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popUserMsg(List<AbstractMessage> messages, UpdateMsgType msgType, String email, Authority authority) {
private boolean popUserMsg(Deque<AbstractMessage> messages, UpdateMsgType msgType, String email, Authority authority) {
for (AbstractMessage message : messages) {
if (message instanceof UserUpdateMsg userUpdateMsg) {
User user = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
@ -1188,7 +1197,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popCustomerMsg(List<AbstractMessage> messages, UpdateMsgType msgType, String title) {
private boolean popCustomerMsg(Deque<AbstractMessage> messages, UpdateMsgType msgType, String title) {
for (AbstractMessage message : messages) {
if (message instanceof CustomerUpdateMsg customerUpdateMsg) {
Customer customer = JacksonUtil.fromString(customerUpdateMsg.getEntity(), Customer.class, true);
@ -1203,7 +1212,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popTenantMsg(List<AbstractMessage> messages, TenantId tenantId1) {
private boolean popTenantMsg(Deque<AbstractMessage> messages, TenantId tenantId1) {
for (AbstractMessage message : messages) {
if (message instanceof TenantUpdateMsg tenantUpdateMsg) {
Tenant tenant = JacksonUtil.fromString(tenantUpdateMsg.getEntity(), Tenant.class, true);
@ -1218,7 +1227,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popTenantProfileMsg(List<AbstractMessage> messages, TenantProfileId tenantProfileId) {
private boolean popTenantProfileMsg(Deque<AbstractMessage> messages, TenantProfileId tenantProfileId) {
for (AbstractMessage message : messages) {
if (message instanceof TenantProfileUpdateMsg tenantProfileUpdateMsg) {
TenantProfile tenantProfile = JacksonUtil.fromString(tenantProfileUpdateMsg.getEntity(), TenantProfile.class, true);
@ -1233,7 +1242,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popSyncCompletedMsg(List<AbstractMessage> messages) {
private boolean popSyncCompletedMsg(Deque<AbstractMessage> messages) {
for (AbstractMessage message : messages) {
if (message instanceof SyncCompletedMsg) {
messages.remove(message);

14
application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java

@ -38,6 +38,8 @@ 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.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.ThingsBoardExecutors;
@ -88,6 +90,8 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_PORT;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_URL;
@TestPropertySource(properties = {
"transport.mqtt.enabled=true",
@ -97,6 +101,12 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@ContextConfiguration(classes = {EntityViewControllerTest.Config.class})
@DaoSqlTest
public class EntityViewControllerTest extends AbstractControllerTest {
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
log.warn("transport.mqtt.bind_port = {}", MQTT_PORT);
registry.add("transport.mqtt.bind_port", () -> MQTT_PORT);
}
static final TypeReference<PageData<EntityView>> PAGE_DATA_ENTITY_VIEW_TYPE_REF = new TypeReference<>() {
};
static final TypeReference<PageData<EntityViewInfo>> PAGE_DATA_ENTITY_VIEW_INFO_TYPE_REF = new TypeReference<>() {
@ -683,7 +693,7 @@ public class EntityViewControllerTest extends AbstractControllerTest {
String viewDeviceId = testDevice.getId().getId().toString();
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId, new MemoryPersistence());
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);
@ -734,7 +744,7 @@ public class EntityViewControllerTest extends AbstractControllerTest {
assertNotNull(accessToken);
String clientId = MqttAsyncClient.generateClientId();
MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId, new MemoryPersistence());
MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);

14
application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java

@ -27,7 +27,10 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.TestSocketUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.Customer;
@ -118,6 +121,15 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
})
@Slf4j
abstract public class AbstractEdgeTest extends AbstractControllerTest {
public static final String EDGE_HOST = "localhost";
public static final int EDGE_PORT = TestSocketUtils.findAvailableTcpPort();
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
log.debug("edges.rpc.port = {}", EDGE_PORT);
registry.add("edges.rpc.port", () -> EDGE_PORT);
}
public static final Integer CONNECT_MESSAGE_COUNT = 17;
public static final Integer INSTALLATION_MESSAGE_COUNT = 8;
public static final Integer SYNC_MESSAGE_COUNT = CONNECT_MESSAGE_COUNT + INSTALLATION_MESSAGE_COUNT;
@ -144,7 +156,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
//8 installation messages
installation();
edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret());
edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret());
// 17 connect messages + 8 installation messages
edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT);
edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class);

10
application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java

@ -22,10 +22,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.JsonObject;
import com.google.protobuf.AbstractMessage;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.adaptor.JsonConverter;
@ -84,12 +87,19 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.gen.edge.v1.UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_PORT;
@TestPropertySource(properties = {
"transport.mqtt.enabled=true"
})
@Slf4j
@DaoSqlTest
public class DeviceEdgeTest extends AbstractEdgeTest {
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
log.warn("transport.mqtt.bind_port = {}", MQTT_PORT);
registry.add("transport.mqtt.bind_port", () -> MQTT_PORT);
}
private static final String DEFAULT_DEVICE_TYPE = "default";

41
application/src/test/java/org/thingsboard/server/edge/EdgeLatestVersionTest.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.edge;
import org.junit.Assert;
import org.junit.Test;
import org.thingsboard.edge.rpc.EdgeVersionComparator;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
public class EdgeLatestVersionTest {
@Test
public void edgeLatestVersionIsSynchronizedTest() {
EdgeVersion currentHighestEdgeVersion = EdgeVersionComparator.getNewestEdgeVersion();
String projectVersion = EdgeLatestVersionTest.class.getPackage().getImplementationVersion();
if (projectVersion == null || projectVersion.isBlank()) {
projectVersion = System.getProperty("project.version", "UNKNOWN");
}
String projectVersionDigits = projectVersion.replaceAll("\\D", "");
String currentHighestEdgeVersionDigits = currentHighestEdgeVersion.name().replaceAll("\\D", "");
String msg = "EdgeVersion enum in edge.proto is out of sync. Please add respective " + projectVersionDigits + " to EdgeVersion";
Assert.assertEquals(msg, projectVersionDigits, currentHighestEdgeVersionDigits);
}
}

19
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -69,10 +69,12 @@ import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@ -103,8 +105,17 @@ public class EdgeImitator {
@Getter
private EdgeConfiguration configuration;
@Getter
private final List<AbstractMessage> downlinkMsgs;
private final ConcurrentLinkedDeque<AbstractMessage> downlinkMsgs;
//Returns collection copy as Unmodifiable list
//This addressing the issue: DeviceEdgeTest>AbstractEdgeTest.setupEdgeTest:212->AbstractEdgeTest.verifyEdgeConnectionAndInitialData:306->AbstractEdgeTest.validateMsgsCnt:387 » ConcurrentModification
public List<AbstractMessage> getDownlinkMsgs() {
return downlinkMsgs.stream().toList();
}
public Deque<AbstractMessage> getDownlinkMsgsDeque() {
return downlinkMsgs;
}
@Getter
private UplinkResponseMsg latestResponseMsg;
@ -113,7 +124,7 @@ public class EdgeImitator {
edgeRpcClient = new EdgeGrpcClient();
messagesLatch = new CountDownLatch(0);
responsesLatch = new CountDownLatch(0);
downlinkMsgs = new ArrayList<>();
downlinkMsgs = new ConcurrentLinkedDeque<>();
ignoredTypes = new ArrayList<>();
this.routingKey = routingKey;
this.routingSecret = routingSecret;
@ -458,7 +469,7 @@ public class EdgeImitator {
}
public AbstractMessage getLatestMessage() {
return downlinkMsgs.get(downlinkMsgs.size() - 1);
return downlinkMsgs.peekLast();
}
public void ignoreType(Class<? extends AbstractMessage> type) {

28
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -238,12 +238,12 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
.set("createAlarm", BooleanNode.TRUE);
postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, attr.toString());
await().atMost(10, TimeUnit.SECONDS)
await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType);
long ts = System.currentTimeMillis();
await().atMost(15, TimeUnit.SECONDS)
await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> clients.values().stream().allMatch(client -> client.getLastDataUpdate() != null));
clients.forEach((expectedDelay, wsClient) -> {
Notification notification = wsClient.getLastDataUpdate().getUpdate();
@ -291,8 +291,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
alarm.setOriginator(device.getId());
alarm = doPost("/api/alarm", alarm, Alarm.class);
await().atMost(15, TimeUnit.SECONDS)
.pollDelay(2, TimeUnit.SECONDS)
await().atMost(TIMEOUT, TimeUnit.SECONDS)
.pollDelay(1, TimeUnit.SECONDS)
.untilAsserted(() -> {
List<Notification> notifications = getMyNotifications(false, 10);
assertThat(notifications).singleElement().matches(notification -> {
@ -345,7 +345,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
.set("createAlarm", BooleanNode.TRUE);
postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, attr.toString());
await().atMost(10, TimeUnit.SECONDS)
await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType);
getWsClient().waitForUpdate(true);
@ -355,14 +355,14 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(notification.getInfo()).asInstanceOf(type(AlarmNotificationInfo.class))
.extracting(AlarmNotificationInfo::getAlarmId).isEqualTo(alarm.getUuidId());
await().atMost(10, TimeUnit.SECONDS).until(() -> findNotificationRequests(EntityType.ALARM).getTotalElements() == escalationTable.size());
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> findNotificationRequests(EntityType.ALARM).getTotalElements() == escalationTable.size());
NotificationRequestInfo scheduledNotificationRequest = findNotificationRequests(EntityType.ALARM).getData().stream()
.filter(NotificationRequest::isScheduled)
.findFirst().orElse(null);
assertThat(scheduledNotificationRequest).extracting(NotificationRequest::getInfo).isEqualTo(notification.getInfo());
alarmSubscriptionService.clearAlarm(tenantId, alarm.getId(), System.currentTimeMillis(), null);
await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty();
});
}
@ -480,7 +480,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
}
loginTenantAdmin();
List<Notification> notifications = await().atMost(15, TimeUnit.SECONDS)
List<Notification> notifications = await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getMyNotifications(true, 10).stream()
.filter(notification -> notification.getType() == NotificationType.RATE_LIMITS)
.collect(Collectors.toList()), list -> list.size() == 3);
@ -500,7 +500,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
});
loginSysAdmin();
notifications = await().atMost(15, TimeUnit.SECONDS)
notifications = await().atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> getMyNotifications(true, 10).stream()
.filter(notification -> notification.getType() == NotificationType.RATE_LIMITS)
.collect(Collectors.toList()), list -> list.size() == 1);
@ -721,7 +721,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
.updateInfo(new UpdateMessage(true, "CHANGED", "test",
"test", "test", "test"))
.build());
await().atMost(5, TimeUnit.SECONDS)
await().atMost(TIMEOUT, TimeUnit.SECONDS)
.untilAsserted(() -> {
assertThat(getMyNotifications(false, 100)).size().isEqualTo(2);
});
@ -809,7 +809,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
method.invoke(systemInfoService);
}
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0);
assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage");
assertThat(notification.getText()).isEqualTo("RAM shortage");
@ -836,7 +836,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
.build());
TimeUnit.MILLISECONDS.sleep(300);
}
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0);
assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage");
assertThat(notification.getText()).isEqualTo("RAM shortage");
@ -848,7 +848,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
.serviceType("serviceType")
.serviceId("serviceId")
.build());
await("").atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getMyNotifications(false, 100)).size().isOne());
await("").atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getMyNotifications(false, 100)).size().isOne());
}
@Test
@ -887,7 +887,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
method.invoke(systemInfoService);
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0);
assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage");
assertThat(notification.getText()).isEqualTo("RAM shortage");

204
application/src/test/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityServiceTest.java

@ -0,0 +1,204 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.system;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.user.UserService;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class DefaultSystemSecurityServiceTest {
@Mock
private AdminSettingsService adminSettingsService;
@Mock
private BCryptPasswordEncoder encoder;
@Mock
private UserService userService;
@Mock
private MailService mailService;
@Mock
private AuditLogService auditLogService;
@Mock
private SecuritySettingsService securitySettingsService;
private DefaultSystemSecurityService systemSecurityService;
private TenantId tenantId;
private UserId userId;
private UserCredentials userCredentials;
private SecuritySettings securitySettings;
private String username;
private String password;
private String encodedPassword;
@Before
public void setUp() {
systemSecurityService = new DefaultSystemSecurityService(adminSettingsService, encoder, userService, mailService, auditLogService, securitySettingsService);
tenantId = TenantId.fromUUID(UUID.randomUUID());
userId = new UserId(UUID.randomUUID());
username = "tenant@example.com";
password = "correctPassword";
encodedPassword = "$2a$10$encodedPasswordHash";
userCredentials = new UserCredentials();
userCredentials.setUserId(userId);
userCredentials.setEnabled(true);
userCredentials.setPassword(encodedPassword);
userCredentials.setCreatedTime(System.currentTimeMillis());
securitySettings = new SecuritySettings();
securitySettings.setMaxFailedLoginAttempts(5);
securitySettings.setPasswordPolicy(new UserPasswordPolicy());
}
@Test
public void testValidateUserCredentials_successfulLogin() {
when(encoder.matches(password, encodedPassword)).thenReturn(true);
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings);
systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password);
verify(encoder).matches(password, encodedPassword);
verify(userService).resetFailedLoginAttempts(tenantId, userId);
verify(userService, never()).increaseFailedLoginAttempts(any(), any());
}
@Test
public void testValidateUserCredentials_wrongPassword_incrementsFailedAttempts() {
when(encoder.matches(password, encodedPassword)).thenReturn(false);
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(3);
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings);
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password))
.isInstanceOf(BadCredentialsException.class)
.hasMessageContaining("Authentication Failed");
verify(userService).increaseFailedLoginAttempts(tenantId, userId);
verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false));
}
@Test
public void testValidateUserCredentials_wrongPassword_accountLocked() {
when(encoder.matches(password, encodedPassword)).thenReturn(false);
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(6);
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings);
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password))
.isInstanceOf(LockedException.class)
.hasMessageContaining("locked due to security policy");
verify(userService).increaseFailedLoginAttempts(tenantId, userId);
verify(userService).setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false);
}
@Test
public void testValidateUserCredentials_wrongPassword_exactlyAtThreshold_noLock() {
when(encoder.matches(password, encodedPassword)).thenReturn(false);
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(5);
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings);
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password))
.isInstanceOf(BadCredentialsException.class)
.hasMessageContaining("Authentication Failed");
verify(userService).increaseFailedLoginAttempts(tenantId, userId);
verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false));
}
@Test
public void testValidateUserCredentials_correctPassword_disabledUser_throwsDisabledException() {
userCredentials.setEnabled(false);
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password))
.isInstanceOf(DisabledException.class)
.hasMessage("User is not active");
verify(encoder, never()).matches(any(), any());
verify(userService, never()).increaseFailedLoginAttempts(any(), any());
}
@Test
public void testValidateUserCredentials_wrongPassword_maxAttemptsDisabled() {
securitySettings.setMaxFailedLoginAttempts(null);
when(encoder.matches(password, encodedPassword)).thenReturn(false);
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(100);
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings);
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password))
.isInstanceOf(BadCredentialsException.class);
verify(userService).increaseFailedLoginAttempts(tenantId, userId);
verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false));
}
@Test
public void testValidateUserCredentials_wrongPassword_maxAttemptsSetToZero() {
securitySettings.setMaxFailedLoginAttempts(0);
when(encoder.matches(password, encodedPassword)).thenReturn(false);
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(100);
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings);
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password))
.isInstanceOf(BadCredentialsException.class);
verify(userService).increaseFailedLoginAttempts(tenantId, userId);
verify(userService, never()).setUserCredentialsEnabled(any(), any(), eq(false));
}
@Test
public void testValidateUserCredentials_wrongPassword_withNotificationEmail() throws ThingsboardException {
String notificationEmail = "admin@example.com";
securitySettings.setUserLockoutNotificationEmail(notificationEmail);
when(encoder.matches(password, encodedPassword)).thenReturn(false);
when(userService.increaseFailedLoginAttempts(tenantId, userId)).thenReturn(6);
when(securitySettingsService.getSecuritySettings()).thenReturn(securitySettings);
assertThatThrownBy(() -> systemSecurityService.validateUserCredentials(tenantId, userCredentials, username, password))
.isInstanceOf(LockedException.class);
verify(userService).setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false);
verify(mailService).sendAccountLockoutEmail(eq(username), eq(notificationEmail), eq(5));
}
}

5
application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java

@ -28,10 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public abstract class AbstractTransportIntegrationTest extends AbstractControllerTest {
protected static final int DEFAULT_WAIT_TIMEOUT_SECONDS = 30;
protected static final String MQTT_URL = "tcp://localhost:1883";
protected static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/";
public static final int DEFAULT_WAIT_TIMEOUT_SECONDS = 30;
protected static final AtomicInteger atomicInteger = new AtomicInteger(2);

14
application/src/test/java/org/thingsboard/server/transport/coap/AbstractCoapIntegrationTest.java

@ -16,7 +16,10 @@
package org.thingsboard.server.transport.coap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.TestSocketUtils;
import org.thingsboard.server.common.data.CoapDeviceType;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -40,6 +43,7 @@ import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadCo
import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.transport.AbstractTransportIntegrationTest;
import org.thingsboard.server.utils.PortFinder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ -52,6 +56,16 @@ import static org.junit.Assert.assertNotNull;
@Slf4j
public abstract class AbstractCoapIntegrationTest extends AbstractTransportIntegrationTest {
public static final String COAP_HOST = "localhost";
public static final int COAP_PORT = PortFinder.findAvailableUdpPort();
public static final String COAP_BASE_URL = "coap://" + COAP_HOST + ":" + COAP_PORT + "/api/v1/";
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
log.warn("coap.bind_port = {}", COAP_PORT);
registry.add("coap.bind_port", () -> COAP_PORT);
}
protected final byte[] EMPTY_PAYLOAD = new byte[0];
protected CoapTestClient client;
protected static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," +

3
application/src/test/java/org/thingsboard/server/transport/coap/CoapTestClient.java

@ -28,9 +28,10 @@ import org.thingsboard.server.common.msg.session.FeatureType;
import java.io.IOException;
import static org.thingsboard.server.transport.coap.AbstractCoapIntegrationTest.COAP_BASE_URL;
public class CoapTestClient {
private static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/";
private static final long CLIENT_REQUEST_TIMEOUT = 60000L;
private final CoapClient client;

37
application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java

@ -21,6 +21,8 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.CoAP;
import org.junit.Assert;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.CoapDeviceType;
@ -37,10 +39,10 @@ import org.thingsboard.server.transport.coap.AbstractCoapIntegrationTest;
import org.thingsboard.server.transport.coap.x509.CertPrivateKey;
import org.thingsboard.server.transport.coap.x509.CoapClientX509Test;
import org.thingsboard.server.transport.coap.CoapTestConfigProperties;
import org.thingsboard.server.utils.PortFinder;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
@ -51,9 +53,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.junit.Assert.assertTrue;
@ -70,7 +70,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
"transport.coap.enabled=true",
})
public abstract class AbstractCoapSecurityIntegrationTest extends AbstractCoapIntegrationTest {
private static final String COAPS_BASE_URL = "coaps://localhost:5684/api/v1/";
public static final String COAPS_HOST = "localhost";
public static final int COAPS_PORT = PortFinder.findAvailableUdpPort();
public static final String COAPS_BASE_URL = "coaps://" + COAPS_HOST + ":" + COAPS_PORT + "/api/v1/";
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
log.warn("coap.dtls.bind_port = {}", COAPS_PORT);
registry.add("coap.dtls.bind_port", () -> COAPS_PORT);
}
protected final String CREDENTIALS_PATH = "coap/credentials/";
protected final String CREDENTIALS_PATH_CLIENT = CREDENTIALS_PATH + "client/";
protected final String CREDENTIALS_PATH_CLIENT_CERT_PEM = CREDENTIALS_PATH_CLIENT + "cert.pem";
@ -160,13 +170,10 @@ public abstract class AbstractCoapSecurityIntegrationTest extends AbstractCoapIn
CertPrivateKey certPrivateKey = new CertPrivateKey(CREDENTIALS_PATH_CLIENT_CERT_PEM, CREDENTIALS_PATH_CLIENT_KEY_PEM);
CertPrivateKey certPrivateKey_01 = new CertPrivateKey(CREDENTIALS_PATH_CLIENT + "cert_01.pem",
CREDENTIALS_PATH_CLIENT + "key_01.pem");
Integer fixedPort = getFreePort();
int fixedPort = PortFinder.findAvailableUdpPort();
CoapClientX509Test clientX509 = clientX509UpdateTest(FeatureType.ATTRIBUTES, certPrivateKey,
"CoapX509TrustNo_" + FeatureType.TELEMETRY.name(), deviceProfile.getId(), fixedPort);
clientX509.disconnect();
await("Need to make port " + fixedPort + " free")
.atMost(40, TimeUnit.SECONDS)
.until(() -> isPortAvailable(fixedPort));
CoapClientX509Test clientX509_01 = clientX509UpdateTest(FeatureType.ATTRIBUTES, certPrivateKey_01,
"CoapX509TrustNo_" + FeatureType.TELEMETRY.name() + "_01", deviceProfile.getId(),
fixedPort, PAYLOAD_VALUES_STR_01);
@ -274,19 +281,5 @@ public abstract class AbstractCoapSecurityIntegrationTest extends AbstractCoapIn
}
}
private static int getFreePort() throws IOException {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
}
}
private static boolean isPortAvailable(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
serverSocket.setReuseAddress(true);
return true;
} catch (IOException e) {
return false;
}
}
}

14
application/src/test/java/org/thingsboard/server/transport/coap/x509/CoapClientX509Test.java

@ -51,6 +51,7 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.awaitility.Awaitility.await;
import static org.eclipse.californium.core.config.CoapConfig.DEFAULT_BLOCKWISE_STATUS_LIFETIME_IN_SECONDS;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_AUTO_HANDSHAKE_TIMEOUT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CIPHER_SUITES;
@ -71,6 +72,8 @@ import static org.eclipse.californium.scandium.config.DtlsConfig.MODULE;
import static org.eclipse.californium.scandium.dtls.SignatureAndHashAlgorithm.SHA256_WITH_ECDSA;
import static org.eclipse.californium.scandium.dtls.SignatureAndHashAlgorithm.SHA256_WITH_RSA;
import static org.eclipse.californium.scandium.dtls.SignatureAndHashAlgorithm.SHA384_WITH_ECDSA;
import static org.thingsboard.server.transport.AbstractTransportIntegrationTest.DEFAULT_WAIT_TIMEOUT_SECONDS;
import static org.thingsboard.server.utils.PortFinder.isUDPPortAvailable;
@Slf4j
public class CoapClientX509Test {
@ -82,6 +85,7 @@ public class CoapClientX509Test {
private final Configuration config;
private final CertPrivateKey certPrivateKey;
private final String coapsBaseUrl;
private final Integer fixedPort;
@Getter
private CoAP.Type type = CoAP.Type.CON;
@ -90,6 +94,7 @@ public class CoapClientX509Test {
this.certPrivateKey = certPrivateKey;
this.coapsBaseUrl = coapsBaseUrl;
this.config = createConfiguration();
this.fixedPort = fixedPort;
this.dtlsConnector = createDTLSConnector(fixedPort);
this.clientX509 = createClient(getFeatureTokenUrl(featureType));
}
@ -97,6 +102,15 @@ public class CoapClientX509Test {
if (clientX509 != null) {
clientX509.shutdown();
}
if (dtlsConnector != null) {
dtlsConnector.destroy();
if (fixedPort != null) {
log.debug("Awaiting releasing UDP fixedPort {}", fixedPort);
await("Await client UDP port " + fixedPort + " to disconnect")
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> isUDPPortAvailable(fixedPort));
}
}
}
public CoapResponse postMethod(String requestBody) throws ConnectorException, IOException {

70
application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java

@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import jnr.ffi.annotations.In;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.awaitility.core.ConditionTimeoutException;
@ -38,6 +39,8 @@ import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
@ -84,9 +87,8 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult;
import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler;
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler;
import org.thingsboard.server.utils.PortFinder;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -141,21 +143,35 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
private LwM2mClientContext clientContextTest;
// Lwm2m Server
public static final int port = 5685;
public static final int securityPort = 5686;
public static final int portBs = 5687;
public static final int securityPortBs = 5688;
public static final String LWM2M_HOST = "localhost";
public static final int LWM2M_PORT = PortFinder.findAvailableUdpPort(); // 5685
public static final int LWM2MS_PORT = PortFinder.findAvailableUdpPort(); //5686
public static final String LWM2M_BOOTSTRAP_HOST = "localhost";
public static final int LWM2M_BOOTSTRAP_PORT = PortFinder.findAvailableUdpPort(); // 5687
public static final int LWM2MS_BOOTSTRAP_PORT = PortFinder.findAvailableUdpPort(); // 5688
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
log.warn("transport.lwm2m.server.bind_port = {}", LWM2M_PORT);
registry.add("transport.lwm2m.server.bind_port", () -> LWM2M_PORT);
log.warn("transport.lwm2m.server.security.bind_port = {}", LWM2MS_PORT);
registry.add("transport.lwm2m.server.security.bind_port", () -> LWM2MS_PORT);
log.warn("transport.lwm2m.bootstrap.bind_port = {}", LWM2M_BOOTSTRAP_PORT);
registry.add("transport.lwm2m.bootstrap.bind_port", () -> LWM2M_BOOTSTRAP_PORT);
log.warn("transport.lwm2m.bootstrap.security.bind_port = {}", LWM2MS_BOOTSTRAP_PORT);
registry.add("transport.lwm2m.bootstrap.security.bind_port", () -> LWM2MS_BOOTSTRAP_PORT);
}
public static final String host = "localhost";
public static final String hostBs = "localhost";
public static final Integer shortServerId = 123;
public static final String COAP = "coap://";
public static final String COAPS = "coaps://";
public static final String URI = COAP + host + ":" + port;
public static final String SECURE_URI = COAPS + host + ":" + securityPort;
public static final String URI_BS = COAP + hostBs + ":" + portBs;
public static final String SECURE_URI_BS = COAPS + hostBs + ":" + securityPortBs;
public static final String URI = COAP + LWM2M_HOST + ":" + LWM2M_PORT;
public static final String SECURE_URI = COAPS + LWM2M_HOST + ":" + LWM2MS_PORT;
public static final String URI_BS = COAP + LWM2M_BOOTSTRAP_HOST + ":" + LWM2M_BOOTSTRAP_PORT;
public static final String SECURE_URI_BS = COAPS + LWM2M_BOOTSTRAP_HOST + ":" + LWM2MS_BOOTSTRAP_PORT;
public static final Security SECURITY_NO_SEC = noSec(URI, shortServerId);
protected final String OBSERVE_ATTRIBUTES_WITHOUT_PARAMS =
@ -570,12 +586,11 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
this.clientDestroy();
lwM2MTestClient = new LwM2MTestClient(this.executor, endpoint, resources);
try (ServerSocket socket = new ServerSocket(0)) {
int clientPort = socket.getLocalPort();
lwM2MTestClient.init(security, securityBs, clientPort, isRpc,
this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest,
clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR, value3_0_9);
}
int clientPort = PortFinder.findAvailableUdpPort();
//automatic client port assignment
lwM2MTestClient.init(security, securityBs, 0 /* clientPort */, isRpc,
this.defaultLwM2mUplinkMsgHandlerTest, this.clientContextTest,
clientDtlsCidLength, queueMode, supportFormatOnly_SenMLJSON_SenMLCBOR, value3_0_9);
lwM2MTestClient.setDeviceIdStr(deviceIdStr);
}
@ -658,10 +673,9 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
try {
if (lwM2MTestClient != null && lwM2MTestClient.getLeshanClient() != null) {
boolean serverAlive = false;
for (int port = AbstractLwM2MIntegrationTest.port; port <= securityPortBs; port++) {
try (ServerSocket socket = new ServerSocket(port)) {
log.info("Port {} is free.", port);
} catch (IOException e) {
List<Integer> ports = List.of(LWM2M_PORT, LWM2MS_PORT, LWM2MS_BOOTSTRAP_PORT, LWM2MS_BOOTSTRAP_PORT);
for (Integer port : ports) {
if (!PortFinder.isUDPPortAvailable(port)) {
log.debug("Port {} is busy — CoAP server still active.", port);
serverAlive = true;
break;
@ -730,8 +744,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
bootstrapServerCredential.setServerPublicKey("");
bootstrapServerCredential.setShortServerId(isBootstrap ? null : shortServerId);
bootstrapServerCredential.setBootstrapServerIs(isBootstrap);
bootstrapServerCredential.setHost(isBootstrap ? hostBs : host);
bootstrapServerCredential.setPort(isBootstrap ? portBs : port);
bootstrapServerCredential.setHost(isBootstrap ? LWM2M_BOOTSTRAP_HOST : LWM2M_HOST);
bootstrapServerCredential.setPort(isBootstrap ? LWM2M_BOOTSTRAP_PORT : LWM2M_PORT);
return bootstrapServerCredential;
}
@ -746,11 +760,15 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte
return credentials;
}
protected void awaitObserveReadAll(int cntObserve, String deviceIdStr) throws Exception {
awaitObserveReadAll(cntObserve, deviceIdStr, "");
}
protected void awaitObserveReadAll(int cntObserve, String deviceIdStr, String assertionAlias) throws Exception {
try {
await("ObserveReadAll: countObserve " + cntObserve)
.atMost(40, TimeUnit.SECONDS)
.alias(assertionAlias)
.atMost(TIMEOUT, TimeUnit.SECONDS)
.until(() -> cntObserve == getCntObserveAll(deviceIdStr));
} catch (ConditionTimeoutException e) {
int current = getCntObserveAll(deviceIdStr);

2
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/FwLwM2MDevice.java

@ -33,7 +33,6 @@ import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicInteger;
import static org.thingsboard.server.dao.service.OtaPackageServiceTest.TARGET_FW_VERSION;
import static org.thingsboard.server.dao.service.OtaPackageServiceTest.TITLE;
@ -137,6 +136,7 @@ public class FwLwM2MDevice extends BaseInstanceEnabler implements Destroyable {
@Override
public void destroy() {
scheduler.shutdownNow();
this.leshanClient = null;
}
private void startDownloading() {

41
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java

@ -33,7 +33,6 @@ import org.eclipse.leshan.client.object.Security;
import org.eclipse.leshan.client.object.Server;
import org.eclipse.leshan.client.observer.LwM2mClientObserver;
import org.eclipse.leshan.client.resource.DummyInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler;
import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
import org.eclipse.leshan.client.resource.ObjectsInitializer;
import org.eclipse.leshan.client.resource.listener.ObjectsListenerAdapter;
@ -82,6 +81,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.awaitility.Awaitility.await;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
@ -92,6 +92,7 @@ import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT;
import static org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder.getDefaultPathEncoder;
import static org.thingsboard.server.transport.AbstractTransportIntegrationTest.DEFAULT_WAIT_TIMEOUT_SECONDS;
import static org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest.shortServerId;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState;
@ -119,6 +120,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INST
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR;
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.lwm2mClientResources;
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.setDtlsConnectorConfigCidLength;
import static org.thingsboard.server.utils.PortFinder.isUDPPortAvailable;
@Slf4j
@ -139,14 +141,16 @@ public class LwM2MTestClient {
private Map<LwM2MClientState, Integer> clientDtlsCid;
private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest;
private LwM2mClientContext clientContext;
private LwM2mTemperatureSensor lwM2mTemperatureSensor12;
private LwM2mTemperatureSensor lwM2MTemperatureSensor12;
private String deviceIdStr;
private int clientPort;
public void init(Security securityLwm2m, Security securityBs, int port, boolean isRpc,
public void init(Security securityLwm2m, Security securityBs, int clientPort, boolean isRpc,
LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler,
LwM2mClientContext clientContext, Integer cIdLength, boolean queueMode,
boolean supportFormatOnly_SenMLJSON_SenMLCBOR, Integer value3_0_9) throws InvalidDDFFileException, IOException {
Assert.assertNull("client already initialized", leshanClient);
this.clientPort = clientPort;
this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler;
this.clientContext = clientContext;
@ -156,7 +160,7 @@ public class LwM2MTestClient {
if (securityLwm2m != null && securityLwm2m.getId() != null) {
forceNullSecurityId(securityLwm2m);
}
if (securityBs!= null && securityBs.getId() != null) {
if (securityBs != null && securityBs.getId() != null) {
forceNullSecurityId(securityBs);
}
if (securityBs != null && securityLwm2m != null) {
@ -165,7 +169,7 @@ public class LwM2MTestClient {
} else if (securityBs != null) {
log.warn("Security BS only: securityBs: [{}] ", securityBs.getId());
initializer.setInstancesForObject(SECURITY, securityBs);
} else if (securityLwm2m != null){
} else if (securityLwm2m != null) {
// SECURITY
log.warn("Security Lwm2m only: security Lwm2m [{}]", securityLwm2m.getId());
initializer.setInstancesForObject(SECURITY, securityLwm2m);
@ -186,8 +190,8 @@ public class LwM2MTestClient {
locationParams.getPos();
initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, OBJECT_INSTANCE_ID_0));
LwM2mTemperatureSensor lwM2mTemperatureSensor0 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0);
lwM2mTemperatureSensor12 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12);
initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2mTemperatureSensor0, lwM2mTemperatureSensor12);
lwM2MTemperatureSensor12 = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12);
initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2mTemperatureSensor0, lwM2MTemperatureSensor12);
List<LwM2mObjectEnabler> enablers = initializer.createAll();
@ -208,7 +212,9 @@ public class LwM2MTestClient {
builder.setSessionListener(new DtlsSessionLogger(clientStates, clientDtlsCid));
return builder;
};
}
;
};
}
};
@ -245,7 +251,7 @@ public class LwM2MTestClient {
// Set Californium Configuration
endpointsBuilder.setConfiguration(clientCoapConfig);
endpointsBuilder.setClientAddress(new InetSocketAddress(port).getAddress());
endpointsBuilder.setClientAddress(new InetSocketAddress(clientPort).getAddress());
// creates EndpointsProvider
@ -438,12 +444,14 @@ public class LwM2MTestClient {
destroySafe(swLwM2MDevice);
destroySafe(lwM2MBinaryAppDataContainer);
destroySafe(lwM2MTemperatureSensor);
destroySafe(lwM2MTemperatureSensor12);
lwM2MDevice = null;
fwLwM2MDevice = null;
swLwM2MDevice = null;
lwM2MBinaryAppDataContainer = null;
lwM2MTemperatureSensor = null;
lwM2MTemperatureSensor12 = null;
}
@ -462,11 +470,17 @@ public class LwM2MTestClient {
public void start(boolean isStartLw) {
if (leshanClient != null) {
if (clientPort > 0) {
log.error("Await UDP clientPort {} to be available before leshanClient.start()", clientPort);
await("Await UDP clientPort " + clientPort + " to be available before leshanClient.start()")
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> isUDPPortAvailable(clientPort));
}
leshanClient.start();
if (isStartLw) {
this.awaitClientAfterStartConnectLw();
}
lwM2mTemperatureSensor12.setLeshanClient(leshanClient);
lwM2MTemperatureSensor12.setLeshanClient(leshanClient);
fwLwM2MDevice.setLeshanClient(leshanClient);
}
}
@ -474,6 +488,12 @@ public class LwM2MTestClient {
public void stop(boolean deregister) {
if (leshanClient != null) {
leshanClient.stop(deregister);
if (clientPort > 0) {
log.error("Await UDP clientPort {} to disconnect after leshanClient.stop(deregister)", clientPort);
await("Await client UDP port " + clientPort + " to disconnect after leshanClient.stop(deregister)")
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.until(() -> isUDPPortAvailable(clientPort));
}
}
}
@ -535,5 +555,6 @@ public class LwM2MTestClient {
log.error("[forceNullSecurityId] Failed to set id=null for {}", security.getClass(), e);
}
}
}

1
application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mTemperatureSensor.java

@ -169,6 +169,7 @@ public class LwM2mTemperatureSensor extends BaseInstanceEnabler implements Destr
@Override
public void destroy() {
this.leshanClient = null;
}
private void sendCollected() {

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java

@ -366,8 +366,8 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M
}
bootstrapServerCredential.setShortServerId(isBootstrap ? null : shortServerId);
bootstrapServerCredential.setBootstrapServerIs(isBootstrap);
bootstrapServerCredential.setHost(isBootstrap ? hostBs : host);
bootstrapServerCredential.setPort(isBootstrap ? securityPortBs : securityPort);
bootstrapServerCredential.setHost(isBootstrap ? LWM2M_BOOTSTRAP_HOST : LWM2M_HOST);
bootstrapServerCredential.setPort(isBootstrap ? LWM2MS_BOOTSTRAP_PORT : LWM2MS_PORT);
return bootstrapServerCredential;
}

4
application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java

@ -133,7 +133,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
ON_REGISTRATION_SUCCESS,
true);
awaitObserveReadAll(1, lwm2mDevice.getId().getId().toString());
awaitObserveReadAll(1, lwm2mDevice.getId().getId().toString(), "before client stops for the first time");
lwM2MTestClient.stop(true);
DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + lwm2mDevice.getDeviceProfileId().getId().toString(), DeviceProfile.class);
@ -143,7 +143,7 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes
Assert.assertNotNull(lwm2mDeviceProfileManyParams);
lwM2MTestClient.start(true);
awaitObserveReadAll(1, lwm2mDevice.getId().getId().toString());
awaitObserveReadAll(1, lwm2mDevice.getId().getId().toString(), "second after client restart");
awaitUpdateReg(3);
}

13
application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java

@ -19,7 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.TestSocketUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
@ -58,6 +61,16 @@ import static org.junit.Assert.assertNotNull;
@Slf4j
public abstract class AbstractMqttIntegrationTest extends AbstractTransportIntegrationTest {
public static final String MQTT_HOST = "localhost";
public static final int MQTT_PORT = TestSocketUtils.findAvailableTcpPort();
public static final String MQTT_URL = "tcp://" + MQTT_HOST + ":" + MQTT_PORT;
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
log.warn("transport.mqtt.bind_port = {}", MQTT_PORT);
registry.add("transport.mqtt.bind_port", () -> MQTT_PORT);
}
protected Device savedGateway;
protected String gatewayAccessToken;

10
application/src/test/java/org/thingsboard/server/transport/mqtt/MqttGatewayRateLimitsTest.java

@ -16,12 +16,15 @@
package org.thingsboard.server.transport.mqtt;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
@ -43,13 +46,20 @@ import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.thingsboard.server.common.data.limit.LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY;
import static org.thingsboard.server.common.data.limit.LimitedApi.TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_PORT;
@DaoSqlTest
@TestPropertySource(properties = {
"service.integrations.supported=ALL",
"transport.mqtt.enabled=true",
})
@Slf4j
public class MqttGatewayRateLimitsTest extends AbstractControllerTest {
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
log.warn("transport.mqtt.bind_port = {}", MQTT_PORT);
registry.add("transport.mqtt.bind_port", () -> MQTT_PORT);
}
private static final String GATEWAY_TOPIC = "v1/gateway/telemetry";
private static final String DEVICE_TOPIC = "v1/devices/me/telemetry";

6
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java

@ -27,12 +27,16 @@ import org.thingsboard.server.common.data.StringUtils;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_HOST;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_PORT;
public class MqttTestClient {
private static final String MQTT_URL = "tcp://localhost:1883";
private static final int TIMEOUT = 30; // seconds
public static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(TIMEOUT);
private String MQTT_URL = "tcp://" + MQTT_HOST + ":" + MQTT_PORT;
private final MqttAsyncClient client;
public void setCallback(MqttTestCallback callback) {

6
application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java

@ -27,12 +27,16 @@ import org.thingsboard.server.common.data.StringUtils;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_HOST;
import static org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest.MQTT_PORT;
public class MqttV5TestClient { // We should copy part of MqttV3TestClient, due to different package names in import
private static final String MQTT_URL = "tcp://localhost:1883";
private static final int TIMEOUT = 30; // seconds
private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(TIMEOUT);
private String MQTT_URL = "tcp://" + MQTT_HOST + ":" + MQTT_PORT;
private final MqttAsyncClient client;
public void setCallback(MqttCallback callback) {

42
application/src/test/java/org/thingsboard/server/utils/PortFinder.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.SocketException;
@Slf4j
public class PortFinder {
public static int findAvailableUdpPort() {
try (DatagramSocket socket = new DatagramSocket(0)) {
return socket.getLocalPort();
} catch (SocketException e) {
throw new IllegalStateException("No available UDP ports found", e);
}
}
public static boolean isUDPPortAvailable(int port) {
try (DatagramSocket socket = new DatagramSocket(port)) {
return true;
} catch (IOException e) {
log.debug("Failed to open UDP port {}", port, e);
return false;
}
}
}

1
application/src/test/resources/application-test.properties

@ -1,3 +1,4 @@
spring.main.banner-mode=off
js.evaluator=mock
transport.lwm2m.server.security.credentials.enabled=true
transport.lwm2m.server.security.credentials.type=KEYSTORE

6
application/src/test/resources/logback-test.xml

@ -42,6 +42,12 @@
<!-- <logger name="org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore" level="TRACE"/>-->
<!-- <logger name="org.thingsboard.server.transport.lwm2m.security.diffPort" level="TRACE"/>-->
<!-- Avoiding multiple logs like: The cache 'relations' is not recording statistics. No meters except 'cache.size' will be registered. Call 'Caffeine#recordStats()' prior to building the cache for metrics to be recorded.-->
<logger name="io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics" level="ERROR"/>
<!-- Log MvcMock result with status >=400 -->
<logger name="org.springframework.test.web.servlet.result" level="DEBUG"/>
<root level="WARN">
<appender-ref ref="console"/>
</root>

2
common/actor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/cache/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

3
common/cache/src/test/resources/logback-test.xml

@ -9,6 +9,9 @@
<logger name="org.thingsboard.server.cache" level="TRACE"/>
<!-- Avoiding multiple logs like: The cache 'relations' is not recording statistics. No meters except 'cache.size' will be registered. Call 'Caffeine#recordStats()' prior to building the cache for metrics to be recorded.-->
<logger name="io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics" level="ERROR"/>
<root level="INFO">
<appender-ref ref="console"/>
</root>

2
common/cluster-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/coap-server/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/dao-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/discovery-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

7
common/edge-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
@ -106,6 +106,11 @@
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

11
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java

@ -143,16 +143,7 @@ public class EdgeGrpcClient implements EdgeRpcClient {
}
public static EdgeVersion getNewestEdgeVersion() {
EdgeVersion newest = null;
for (EdgeVersion v : EdgeVersion.values()) {
if (v == EdgeVersion.V_LATEST || v == EdgeVersion.UNRECOGNIZED) {
continue;
}
if (newest == null || v.getNumber() > newest.getNumber()) {
newest = v;
}
}
return newest;
return EdgeVersionComparator.getNewestEdgeVersion();
}
private StreamObserver<ResponseMsg> initOutputStream(String edgeKey,

86
common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeVersionComparator.java

@ -0,0 +1,86 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.edge.rpc;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import java.util.Comparator;
public class EdgeVersionComparator implements Comparator<EdgeVersion> {
public static final EdgeVersionComparator INSTANCE = new EdgeVersionComparator();
@Override
public int compare(EdgeVersion v1, EdgeVersion v2) {
if (v1 == v2) {
return 0;
}
// UNRECOGNIZED is less than any other version
if (v1 == EdgeVersion.UNRECOGNIZED) {
return -1;
}
if (v2 == EdgeVersion.UNRECOGNIZED) {
return 1;
}
// V_LATEST is treated as the newest version
if (v1 == EdgeVersion.V_LATEST) {
v1 = getNewestEdgeVersion();
}
if (v2 == EdgeVersion.V_LATEST) {
v2 = getNewestEdgeVersion();
}
return compareVersionParts(parseVersionParts(v1), parseVersionParts(v2));
}
public static EdgeVersion getNewestEdgeVersion() {
EdgeVersion newest = null;
for (EdgeVersion v : EdgeVersion.values()) {
if (v == EdgeVersion.V_LATEST || v == EdgeVersion.UNRECOGNIZED) {
continue;
}
if (newest == null || INSTANCE.compare(v, newest) > 0) {
newest = v;
}
}
return newest;
}
private static int[] parseVersionParts(EdgeVersion version) {
String name = version.name();
if (name.startsWith("V_")) {
name = name.substring(2);
}
String[] parts = name.split("_");
int[] result = new int[parts.length];
for (int i = 0; i < parts.length; i++) {
result[i] = Integer.parseInt(parts[i]);
}
return result;
}
private static int compareVersionParts(int[] a, int[] b) {
int maxLen = Math.max(a.length, b.length);
for (int i = 0; i < maxLen; i++) {
int partA = i < a.length ? a[i] : 0;
int partB = i < b.length ? b[i] : 0;
if (partA != partB) {
return Integer.compare(partA, partB);
}
}
return 0;
}
}

2
common/edge-api/src/main/proto/edge.proto

@ -45,6 +45,8 @@ enum EdgeVersion {
V_4_1_0 = 11;
V_4_2_0 = 12;
V_4_3_0 = 13;
V_4_2_1_2 = 14;
V_4_3_0_1 = 15;
V_LATEST = 999;
}

102
common/edge-api/src/test/java/org/thingsboard/edge/rpc/EdgeVersionComparatorTest.java

@ -0,0 +1,102 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.edge.rpc;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import static org.assertj.core.api.Assertions.assertThat;
class EdgeVersionComparatorTest {
@Test
void compare_sameVersion_returnsZero() {
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_3_0, EdgeVersion.V_3_3_0)).isEqualTo(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_0_0, EdgeVersion.V_4_0_0)).isEqualTo(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_2_1_2, EdgeVersion.V_4_2_1_2)).isEqualTo(0);
}
@Test
void compare_majorVersionDifference_returnsCorrectOrder() {
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_3_0, EdgeVersion.V_4_0_0)).isLessThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_0_0, EdgeVersion.V_3_3_0)).isGreaterThan(0);
}
@Test
void compare_minorVersionDifference_returnsCorrectOrder() {
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_3_0, EdgeVersion.V_3_6_0)).isLessThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_6_0, EdgeVersion.V_3_3_0)).isGreaterThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_0_0, EdgeVersion.V_4_1_0)).isLessThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_1_0, EdgeVersion.V_4_0_0)).isGreaterThan(0);
}
@Test
void compare_patchVersionDifference_returnsCorrectOrder() {
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_6_0, EdgeVersion.V_3_6_1)).isLessThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_6_1, EdgeVersion.V_3_6_0)).isGreaterThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_6_1, EdgeVersion.V_3_6_2)).isLessThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_6_2, EdgeVersion.V_3_6_4)).isLessThan(0);
}
@Test
void compare_fourPartVersion_returnsCorrectOrder() {
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_2_0, EdgeVersion.V_4_2_1_2)).isLessThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_2_1_2, EdgeVersion.V_4_2_0)).isGreaterThan(0);
}
@Test
void compare_threePartVsFourPart_treatsImplicitZero() {
// V_4_2_0 should be less than V_4_2_1_2 (4.2.0.0 < 4.2.1.2)
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_4_2_0, EdgeVersion.V_4_2_1_2)).isLessThan(0);
}
@Test
void getNewestEdgeVersion_excludesLatestAndUnrecognized() {
EdgeVersion newest = EdgeVersionComparator.getNewestEdgeVersion();
assertThat(newest).isNotNull();
assertThat(newest).isNotEqualTo(EdgeVersion.V_LATEST);
assertThat(newest).isNotEqualTo(EdgeVersion.UNRECOGNIZED);
}
@Test
void compare_vLatest_treatedAsNewestVersion() {
EdgeVersion newest = EdgeVersionComparator.getNewestEdgeVersion();
// V_LATEST equals the newest version
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_LATEST, newest)).isEqualTo(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(newest, EdgeVersion.V_LATEST)).isEqualTo(0);
// V_LATEST is greater than older versions
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_LATEST, EdgeVersion.V_3_3_0)).isGreaterThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_3_0, EdgeVersion.V_LATEST)).isLessThan(0);
}
@Test
void compare_vLatest_withItself_returnsZero() {
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_LATEST, EdgeVersion.V_LATEST)).isEqualTo(0);
}
@Test
void compare_unrecognized_isLessThanAnyVersion() {
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.UNRECOGNIZED, EdgeVersion.V_3_3_0)).isLessThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.UNRECOGNIZED, EdgeVersion.V_LATEST)).isLessThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_3_3_0, EdgeVersion.UNRECOGNIZED)).isGreaterThan(0);
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.V_LATEST, EdgeVersion.UNRECOGNIZED)).isGreaterThan(0);
}
@Test
void compare_unrecognized_withItself_returnsZero() {
assertThat(EdgeVersionComparator.INSTANCE.compare(EdgeVersion.UNRECOGNIZED, EdgeVersion.UNRECOGNIZED)).isEqualTo(0);
}
}

2
common/edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

3
common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbRocksDb.java

@ -16,6 +16,7 @@
package org.thingsboard.server.edqs.util;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.rocksdb.Options;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksIterator;
@ -26,6 +27,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.BiConsumer;
@Slf4j
public class TbRocksDb {
protected final String path;
@ -45,6 +47,7 @@ public class TbRocksDb {
@SneakyThrows
public void init() {
log.debug("RocksDB init in {}", path);
Files.createDirectories(Path.of(path).getParent());
db = RocksDB.open(dbOptions, path);
}

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>common</artifactId>

2
common/proto/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/queue/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/script/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/script/remote-js-client/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>

2
common/script/script-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>script</artifactId>
</parent>
<groupId>org.thingsboard.common.script</groupId>

2
common/stats/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/transport/snmp/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>

2
common/transport/transport-api/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.common</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.common.transport</groupId>

2
common/util/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
common/version-control/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>dao</artifactId>

7
dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java

@ -19,6 +19,7 @@ import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.util.concurrent.FluentFuture;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -55,6 +56,12 @@ public class DefaultTbResourceDataCache implements TbResourceDataCache {
.buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> resourceService.getResourceDataInfo(key.tenantId(), key.resourceId()), executor));
}
@PreDestroy
private void destroy() {
cache.synchronous().invalidateAll();
cache = null;
}
@Override
public FluentFuture<TbResourceDataInfo> getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId) {
log.trace("Retrieving resource data info by id [{}], tenant id [{}] from cache", resourceId, tenantId);

16
dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java

@ -100,6 +100,18 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
@Autowired
private DashboardService dashboardService;
@Value("${transport.lwm2m.server.bind_port:5685}")
private Integer lwm2mPort;
@Value("${transport.lwm2m.server.security.bind_port:5686}")
private Integer lwm2mSecurePort;
@Value("${transport.lwm2m.bootstrap.bind_port:5687}")
private Integer lwm2mBootstrapPort;
@Value("${transport.lwm2m.bootstrap.security.bind_port:5688}")
private Integer lwm2mBootstrapSecurePort;
@Value("${security.java_cacerts.path:}")
private String javaCacertsPath;
@ -371,9 +383,9 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator<D
}
int port;
if (LwM2MSecurityMode.NO_SEC.equals(serverConfig.getSecurityMode())) {
port = serverConfig.isBootstrapServerIs() ? 5687 : 5685;
port = serverConfig.isBootstrapServerIs() ? lwm2mBootstrapPort : lwm2mPort;
} else {
port = serverConfig.isBootstrapServerIs() ? 5688 : 5686;
port = serverConfig.isBootstrapServerIs() ? lwm2mBootstrapSecurePort : lwm2mSecurePort;
}
if (serverConfig.getPort() == null || serverConfig.getPort() != port) {
throw new DeviceCredentialsValidationException(server + " \"Port\" value = " + serverConfig.getPort() + ". This value for security " + serverConfig.getSecurityMode().name() + " must be " + port + "!");

16
dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java

@ -16,16 +16,12 @@
package org.thingsboard.server.dao.service;
import org.junit.Assert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.exception.DataValidationException;
class ConstraintValidatorTest {
private static final int MIN_IN_MS = 60000;
private static final int _1M = 1_000_000;
@Test
void validateFields() {
StringDataEntry stringDataEntryValid = new StringDataEntry("key", "value");
@ -35,16 +31,4 @@ class ConstraintValidatorTest {
ConstraintValidator.validateFields(stringDataEntryValid);
}
@Test
void validatePerMinute() {
StringDataEntry stringDataEntryValid = new StringDataEntry("key", "value");
long start = System.currentTimeMillis();
for (int i = 0; i < _1M; i++) {
ConstraintValidator.validateFields(stringDataEntryValid);
}
long end = System.currentTimeMillis();
Assertions.assertTrue(MIN_IN_MS > end - start);
}
}

154
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/sql/LatestTimeseriesPerformanceTest.java

@ -1,154 +0,0 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.service.timeseries.sql;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.service.AbstractServiceTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@DaoSqlTest
@Slf4j
public class LatestTimeseriesPerformanceTest extends AbstractServiceTest {
private static final String STRING_KEY = "stringKey";
private static final String LONG_KEY = "longKey";
private static final String DOUBLE_KEY = "doubleKey";
private static final String BOOLEAN_KEY = "booleanKey";
private static final int AMOUNT_OF_UNIQ_KEY = 10000;
private static final int TIMEOUT = 100;
private final Random random = new Random();
@Autowired
private TimeseriesLatestDao timeseriesLatestDao;
private ListeningExecutorService testExecutor;
private EntityId entityId;
private AtomicLong saveCounter;
@Before
public void before() {
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
Tenant savedTenant = tenantService.saveTenant(tenant);
Assert.assertNotNull(savedTenant);
tenantId = savedTenant.getId();
entityId = new DeviceId(UUID.randomUUID());
saveCounter = new AtomicLong(0);
testExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(200, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope")));
}
@After
public void after() {
tenantService.deleteTenant(tenantId);
if (testExecutor != null) {
testExecutor.shutdownNow();
}
}
@Test
public void test_save_latest_timeseries() throws Exception {
warmup();
saveCounter.set(0);
long startTime = System.currentTimeMillis();
List<ListenableFuture<?>> futures = new ArrayList<>();
for (int i = 0; i < 25_000; i++) {
futures.add(save(generateStrEntry(getRandomKey())));
futures.add(save(generateLngEntry(getRandomKey())));
futures.add(save(generateDblEntry(getRandomKey())));
futures.add(save(generateBoolEntry(getRandomKey())));
}
Futures.allAsList(futures).get(TIMEOUT, TimeUnit.SECONDS);
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
log.info("Total time: {}", totalTime);
log.info("Saved count: {}", saveCounter.get());
log.warn("Saved per 1 sec: {}", saveCounter.get() * 1000 / totalTime);
}
private void warmup() throws Exception {
List<ListenableFuture<?>> futures = new ArrayList<>();
for (int i = 0; i < AMOUNT_OF_UNIQ_KEY; i++) {
futures.add(save(generateStrEntry(i)));
futures.add(save(generateLngEntry(i)));
futures.add(save(generateDblEntry(i)));
futures.add(save(generateBoolEntry(i)));
}
Futures.allAsList(futures).get(TIMEOUT, TimeUnit.SECONDS);
}
private ListenableFuture<?> save(TsKvEntry tsKvEntry) {
return Futures.transformAsync(testExecutor.submit(() -> timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry)), result -> {
saveCounter.incrementAndGet();
return result;
}, testExecutor);
}
private TsKvEntry generateStrEntry(int keyIndex) {
return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(STRING_KEY + keyIndex, RandomStringUtils.random(10)));
}
private TsKvEntry generateLngEntry(int keyIndex) {
return new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(LONG_KEY + keyIndex, random.nextLong()));
}
private TsKvEntry generateDblEntry(int keyIndex) {
return new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry(DOUBLE_KEY + keyIndex, random.nextDouble()));
}
private TsKvEntry generateBoolEntry(int keyIndex) {
return new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry(BOOLEAN_KEY + keyIndex, random.nextBoolean()));
}
private int getRandomKey() {
return random.nextInt(AMOUNT_OF_UNIQ_KEY);
}
}

1
dao/src/test/resources/application-test.properties

@ -1,3 +1,4 @@
spring.main.banner-mode=off
zk.enabled=false
zk.url=localhost:2181
zk.zk_dir=/thingsboard

3
dao/src/test/resources/logback-test.xml

@ -19,6 +19,9 @@
<!-- <logger name="org.hibernate.SQL" level="DEBUG"/> -->
<!-- <logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG" /> -->
<!-- Avoiding multiple logs like: The cache 'relations' is not recording statistics. No meters except 'cache.size' will be registered. Call 'Caffeine#recordStats()' prior to building the cache for metrics to be recorded.-->
<logger name="io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics" level="ERROR"/>
<root level="WARN">
<appender-ref ref="console"/>
</root>

2
edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>edqs</artifactId>

2
monitoring/pom.xml

@ -21,7 +21,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>

2
msa/black-box-tests/pom.xml

@ -21,7 +21,7 @@
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

4
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractCoapClientTest.java

@ -28,7 +28,7 @@ import org.thingsboard.server.common.msg.session.FeatureType;
public abstract class AbstractCoapClientTest extends AbstractContainerTest{
private static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/";
private static final String CONTAINER_COAP_BASE_URL = "coap://localhost:5683/api/v1/";
private static final long CLIENT_REQUEST_TIMEOUT = 60000L;
@ -54,7 +54,7 @@ public abstract class AbstractCoapClientTest extends AbstractContainerTest{
protected byte[] createCoapClientAndPublish(String deviceName) throws Exception {
String provisionRequestMsg = createTestProvisionMessage(deviceName);
Configuration.addDefaultModule(MODULE_DEFINITIONS_PROVIDER);
String featureTokenUrl = COAP_BASE_URL + FeatureType.PROVISION.name().toLowerCase();
String featureTokenUrl = CONTAINER_COAP_BASE_URL + FeatureType.PROVISION.name().toLowerCase();
client = new CoapClient(featureTokenUrl);
return client.setTimeout(CLIENT_REQUEST_TIMEOUT)
.post(provisionRequestMsg.getBytes(), MediaTypeRegistry.APPLICATION_JSON)

42
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/PortFinder.java

@ -0,0 +1,42 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.msa;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.SocketException;
@Slf4j
public class PortFinder {
public static int findAvailableUdpPort() {
try (DatagramSocket socket = new DatagramSocket(0)) {
return socket.getLocalPort();
} catch (SocketException e) {
throw new IllegalStateException("No available UDP ports found", e);
}
}
public static boolean isUDPPortAvailable(int port) {
try (DatagramSocket socket = new DatagramSocket(port)) {
return true;
} catch (IOException e) {
log.debug("Failed to open UDP port {}", port, e);
return false;
}
}
}

24
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java

@ -810,18 +810,27 @@ public class CalculatedFieldTest extends AbstractContainerTest {
// --- Build CF: PROPAGATION ---
CalculatedField saved = createPropagationCF(device.getId());
// Wait for INITIALIZED event
waitForDebugEvent(saved, CalculatedFieldEventType.INITIALIZED.name(), 1);
// Create relations FROM asset 2 TO device
Asset asset2 = testRestClient.postAsset(createAsset("Propagated Asset 2", null));
EntityRelation rel2 = new EntityRelation(asset2.getId(), device.getId(), EntityRelation.CONTAINS_TYPE);
testRestClient.postEntityRelation(rel2);
// Wait for RELATION_ADD_OR_UPDATE event
waitForDebugEvent(saved, CalculatedFieldEventType.RELATION_ADD_OR_UPDATE.name(), 2);
// Telemetry on device
testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25.1}"));
// Wait for POST_TELEMETRY_REQUEST event
waitForDebugEvent(saved, TbMsgType.POST_TELEMETRY_REQUEST.name(), 3);
// Delete relation between asset 1 and device
testRestClient.deleteEntityRelation(asset1.getId(), EntityRelation.CONTAINS_TYPE, device.getId());
// --- Assert propagated calculation (arguments-only mode) ---
// --- Assert all debug events in correct sequence ---
await().alias("check debug events")
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
@ -845,6 +854,19 @@ public class CalculatedFieldTest extends AbstractContainerTest {
testRestClient.deleteAsset(asset2.getId());
}
private void waitForDebugEvent(CalculatedField cf, String expectedEventType, int expectedTotalEvents) {
await().alias("wait for debug event: " + expectedEventType)
.atMost(TIMEOUT, TimeUnit.SECONDS)
.pollInterval(POLL_INTERVAL, TimeUnit.SECONDS)
.untilAsserted(() -> {
List<String> eventTypes = testRestClient.getEvents(cf.getId(), EventType.DEBUG_CALCULATED_FIELD, tenantId, new TimePageLink(expectedTotalEvents, 0, null, SortOrder.BY_CREATED_TIME_DESC)).getData().stream()
.map(e -> e.getBody().get("msgType").asText())
.toList();
assertThat(eventTypes).hasSize(expectedTotalEvents);
assertThat(eventTypes.get(0)).isEqualTo(expectedEventType);
});
}
private CalculatedField createPropagationCF(EntityId entityId) {
CalculatedField cf = new CalculatedField();
cf.setEntityId(entityId);

8
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/AbstractLwm2mClientTest.java

@ -57,12 +57,12 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.msa.AbstractContainerTest;
import org.thingsboard.server.msa.PortFinder;
import org.thingsboard.server.msa.WsClient;
import org.thingsboard.server.msa.connectivity.lwm2m.client.LwM2MTestClient;
import org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState;
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
@ -229,10 +229,8 @@ public class AbstractLwm2mClientTest extends AbstractContainerTest {
String endpoint, ScheduledExecutorService executor) throws Exception {
this.executor = executor;
LwM2MTestClient lwM2MTestClient = new LwM2MTestClient(executor, endpoint);
try (ServerSocket socket = new ServerSocket(0)) {
int clientPort = socket.getLocalPort();
lwM2MTestClient.init(security, clientPort);
}
int clientPort = PortFinder.findAvailableUdpPort();
lwM2MTestClient.init(security, clientPort);
return lwM2MTestClient;
}

21
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/client/LwM2MTestClient.java

@ -65,6 +65,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY;
import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL;
@ -72,6 +73,7 @@ import static org.eclipse.leshan.core.LwM2mId.DEVICE;
import static org.eclipse.leshan.core.LwM2mId.FIRMWARE;
import static org.eclipse.leshan.core.LwM2mId.SECURITY;
import static org.eclipse.leshan.core.LwM2mId.SERVER;
import static org.thingsboard.server.msa.PortFinder.isUDPPortAvailable;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_FAILURE;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED;
@ -95,7 +97,6 @@ import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelp
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.serverId;
import static org.thingsboard.server.msa.connectivity.lwm2m.client.Lwm2mTestHelper.shortServerId;
@Slf4j
@Data
public class LwM2MTestClient {
@ -115,8 +116,11 @@ public class LwM2MTestClient {
private int countUpdateRegistrationSuccess;
private int countReadObserveAfterUpdateRegistrationSuccess;
private int clientPort;
public void init(Security security, int clientPort) throws InvalidDDFFileException, IOException {
assertThat(leshanClient).as("client already initialized").isNull();
this.clientPort = clientPort;
List<ObjectModel> models = new ArrayList<>();
for (String resourceName : resources) {
@ -342,12 +346,27 @@ public class LwM2MTestClient {
}
});
}
public void start() {
leshanClient.start();
if (clientPort > 0) {
log.error("Await UDP clientPort {} to be available before leshanClient.start()", clientPort);
await("Await UDP clientPort " + clientPort + " to be available before leshanClient.start()")
.atMost(30, TimeUnit.SECONDS)
.until(() -> isUDPPortAvailable(clientPort));
}
}
public void destroy() {
if (leshanClient != null) {
leshanClient.destroy(true);
if (clientPort > 0) {
log.error("Await UDP clientPort {} to disconnect after leshanClient.stop(deregister)", clientPort);
await("Await client UDP port " + clientPort + " to disconnect after leshanClient.stop(deregister)")
.atMost(30, TimeUnit.SECONDS)
.until(() -> isUDPPortAvailable(clientPort));
}
}
if (lwM2MDevice != null) {
lwM2MDevice.destroy();

3
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/rule/node/MqttNodeTest.java

@ -65,6 +65,7 @@ import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevi
public class MqttNodeTest extends AbstractContainerTest {
private static final String TOPIC = "tb/mqtt/device";
private static final String CONTAINER_MQTT_URL = "tcp://localhost:1883";
private Device device;
@ -94,7 +95,7 @@ public class MqttNodeTest extends AbstractContainerTest {
responseClient.connect();
responseClient.subscribe(TOPIC, messageListener);
MqttClient mqttClient = new MqttClient("tcp://localhost:1883", StringUtils.randomAlphanumeric(10), new MemoryPersistence());
MqttClient mqttClient = new MqttClient(CONTAINER_MQTT_URL, StringUtils.randomAlphanumeric(10), new MemoryPersistence());
MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
mqttConnectOptions.setUserName(deviceCredentials.getCredentialsId());
mqttClient.connect(mqttConnectOptions);

2
msa/edqs/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/js-executor/package.json

@ -1,7 +1,7 @@
{
"name": "thingsboard-js-executor",
"private": true,
"version": "4.3.0",
"version": "4.3.0.1",
"description": "ThingsBoard JavaScript Executor Microservice",
"main": "server.ts",
"bin": "server.js",

2
msa/js-executor/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/monitoring/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>

2
msa/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>thingsboard</artifactId>
</parent>
<artifactId>msa</artifactId>

2
msa/tb-node/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/tb/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>msa</artifactId>
</parent>
<groupId>org.thingsboard.msa</groupId>

2
msa/transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

2
msa/transport/lwm2m/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard.msa</groupId>
<version>4.3.0-RC</version>
<version>4.3.0.1-SNAPSHOT</version>
<artifactId>transport</artifactId>
</parent>
<groupId>org.thingsboard.msa.transport</groupId>

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save