Browse Source

Merge remote-tracking branch 'upstream/develop/3.5' into mqtt5-codes

pull/7596/head
imbeacon 4 years ago
parent
commit
0cbf163e8f
  1. 8
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  2. 13
      application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json
  3. 8
      application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
  4. 1
      application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java
  5. 4
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  6. 16
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  7. 49
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  8. 6
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  9. 28
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  10. 6
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  11. 6
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  12. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  14. 15
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  15. 4
      application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
  16. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  17. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  18. 3
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  19. 23
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  20. 6
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java
  21. 162
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java
  22. 69
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java
  23. 39
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java
  24. 35
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java
  25. 23
      application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java
  26. 17
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  27. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  28. 32
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  29. 38
      application/src/main/resources/thingsboard.yml
  30. 122
      application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java
  31. 4
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  32. 114
      application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java
  33. 10
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  34. 68
      application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandlerTest.java
  35. 2
      application/src/test/resources/logback-test.xml
  36. 19
      common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java
  37. 2
      common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java
  38. 2
      common/data/src/main/java/org/thingsboard/server/common/data/script/ScriptLanguage.java
  39. 9
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java
  40. 30
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java
  41. 6
      common/script/script-api/pom.xml
  42. 112
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java
  43. 4
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
  44. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbJson.java
  45. 104
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  46. 6
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelInvokeService.java
  47. 6
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelScript.java
  48. 8
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelScriptExecutionTask.java
  49. 11
      dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java
  50. 19
      dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java
  51. 17
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java
  52. 5
      dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java
  53. 8
      dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java
  54. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  55. 79
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  56. 3
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java
  57. 39
      dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java
  58. 133
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningDaysAlwaysExistsTest.java
  59. 134
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningHoursAlwaysExistsTest.java
  60. 78
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningIndefiniteAlwaysExistsTest.java
  61. 121
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java
  62. 134
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java
  63. 116
      dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java
  64. 3
      docker/tb-js-executor.env
  65. 1
      lombok.config
  66. 4
      msa/black-box-tests/README.md
  67. 38
      msa/black-box-tests/pom.xml
  68. 130
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java
  69. 221
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java
  70. 53
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java
  71. 61
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java
  72. 262
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java
  73. 9
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java
  74. 112
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java
  75. 226
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
  76. 220
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java
  77. 40
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java
  78. 2
      msa/black-box-tests/src/test/resources/config.properties
  79. 27
      msa/black-box-tests/src/test/resources/testNG.xml
  80. 2
      msa/js-executor/api/jsExecutor.models.ts
  81. 11
      msa/js-executor/api/jsInvokeMessageProcessor.ts
  82. 1
      msa/js-executor/config/custom-environment-variables.yml
  83. 1
      msa/js-executor/config/default.yml
  84. 2
      msa/js-executor/docker/start-js-executor.sh
  85. 4
      packaging/java/scripts/install/logback.xml
  86. 34
      pom.xml
  87. 2
      pull_request_template.md
  88. 26
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  89. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
  90. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java
  91. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java
  92. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
  93. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java
  94. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java
  95. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java
  96. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java
  97. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java
  98. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
  99. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java
  100. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java

8
application/src/main/data/json/tenant/device_profile/rule_chain_template.json

@ -57,9 +57,9 @@
"name": "Log RPC from Device",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{
@ -71,9 +71,9 @@
"name": "Log Other",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{

13
application/src/main/data/json/tenant/edge_management/rule_chains/edge_root_rule_chain.json

@ -70,9 +70,9 @@
"name": "Log RPC from Device",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{
@ -84,9 +84,9 @@
"name": "Log Other",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{
@ -174,11 +174,6 @@
"fromIndex": 3,
"toIndex": 7,
"type": "Timeseries Updated"
},
{
"fromIndex": 4,
"toIndex": 7,
"type": "Success"
}
],
"ruleChainConnections": null

8
application/src/main/data/json/tenant/rule_chains/root_rule_chain.json

@ -57,9 +57,9 @@
"name": "Log RPC from Device",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{
@ -71,9 +71,9 @@
"name": "Log Other",
"debugMode": false,
"configuration": {
"scriptLang": "MVEL",
"scriptLang": "TBEL",
"jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);",
"mvelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
"tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
}
},
{

1
application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java

@ -29,6 +29,7 @@ import java.util.Arrays;
@ComponentScan({"org.thingsboard.server.install",
"org.thingsboard.server.service.component",
"org.thingsboard.server.service.install",
"org.thingsboard.server.service.security.auth.jwt.settings",
"org.thingsboard.server.dao",
"org.thingsboard.server.common.stats",
"org.thingsboard.server.common.transport.config.ssl",

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

@ -33,7 +33,7 @@ import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.cluster.TbClusterService;
@ -274,7 +274,7 @@ public class ActorSystemContext {
@Autowired(required = false)
@Getter
private MvelInvokeService mvelInvokeService;
private TbelInvokeService tbelInvokeService;
@Autowired
@Getter

16
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -97,7 +97,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine;
import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine;
import java.util.Collections;
import java.util.List;
@ -474,11 +474,11 @@ class DefaultTbContext implements TbContext {
return new RuleNodeJsScriptEngine(getTenantId(), mainCtx.getJsInvokeService(), script, argNames);
}
private ScriptEngine createMvelScriptEngine(String script, String... argNames) {
if (mainCtx.getMvelInvokeService() == null) {
throw new RuntimeException("MVEL execution is disabled!");
private ScriptEngine createTbelScriptEngine(String script, String... argNames) {
if (mainCtx.getTbelInvokeService() == null) {
throw new RuntimeException("TBEL execution is disabled!");
}
return new RuleNodeMvelScriptEngine(getTenantId(), mainCtx.getMvelInvokeService(), script, argNames);
return new RuleNodeTbelScriptEngine(getTenantId(), mainCtx.getTbelInvokeService(), script, argNames);
}
@Override
@ -492,11 +492,11 @@ class DefaultTbContext implements TbContext {
switch (scriptLang) {
case JS:
return createJsScriptEngine(script, argNames);
case MVEL:
case TBEL:
if (Arrays.isNullOrEmpty(argNames)) {
return createMvelScriptEngine(script, "msg", "metadata", "msgType");
return createTbelScriptEngine(script, "msg", "metadata", "msgType");
} else {
return createMvelScriptEngine(script, argNames);
return createTbelScriptEngine(script, argNames);
}
default:
throw new RuntimeException("Unsupported script language: " + scriptLang.name());

49
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -22,7 +22,9 @@ import com.google.common.util.concurrent.MoreExecutors;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
@ -37,8 +39,13 @@ import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
import org.thingsboard.server.common.data.sync.vc.AutoCommitSettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettings;
import org.thingsboard.server.common.data.sync.vc.RepositorySettingsInfo;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -65,6 +72,14 @@ public class AdminController extends BaseController {
@Autowired
private SystemSecurityService systemSecurityService;
@Lazy
@Autowired
private JwtSettingsService jwtSettingsService;
@Lazy
@Autowired
private JwtTokenFactory tokenFactory;
@Autowired
private EntitiesVersionControlService versionControlService;
@ -152,6 +167,40 @@ public class AdminController extends BaseController {
}
}
@ApiOperation(value = "Get the JWT Settings object (getJwtSettings)",
notes = "Get the JWT Settings object that contains JWT token policy, etc. " + SYSTEM_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.GET)
@ResponseBody
public JwtSettings getJwtSettings() throws ThingsboardException {
try {
accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
return checkNotNull(jwtSettingsService.getJwtSettings());
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Update JWT Settings (saveJwtSettings)",
notes = "Updates the JWT Settings object that contains JWT token policy, etc. The tokenSigningKey field is a Base64 encoded string." + SYSTEM_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/jwtSettings", method = RequestMethod.POST)
@ResponseBody
public JwtPair saveJwtSettings(
@ApiParam(value = "A JSON value representing the JWT Settings.")
@RequestBody JwtSettings jwtSettings) throws ThingsboardException {
try {
SecurityUser securityUser = getCurrentUser();
accessControlService.checkPermission(securityUser, Resource.ADMIN_SETTINGS, Operation.WRITE);
checkNotNull(jwtSettingsService.saveJwtSettings(jwtSettings));
return tokenFactory.createTokenPair(securityUser);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Send test email (sendTestMail)",
notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " +
"You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH)

6
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -51,7 +51,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest;
import org.thingsboard.server.service.security.model.ResetPasswordRequest;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -236,7 +236,7 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JwtTokenPair activateUser(
public JwtPair activateUser(
@ApiParam(value = "Activate user request.")
@RequestBody ActivateUserRequest activateRequest,
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
@ -278,7 +278,7 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JwtTokenPair resetPassword(
public JwtPair resetPassword(
@ApiParam(value = "Reset password request.")
@RequestBody ResetPasswordRequest resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException {

28
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -38,7 +38,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.script.api.js.JsInvokeService;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.EventInfo;
@ -69,7 +69,7 @@ import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.script.RuleNodeMvelScriptEngine;
import org.thingsboard.server.service.script.RuleNodeTbelScriptEngine;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@ -146,7 +146,7 @@ public class RuleChainController extends BaseController {
private JsInvokeService jsInvokeService;
@Autowired(required = false)
private MvelInvokeService mvelInvokeService;
private TbelInvokeService tbelInvokeService;
@Autowired(required = false)
private ActorSystemContext actorContext;
@ -154,8 +154,8 @@ public class RuleChainController extends BaseController {
@Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}")
private boolean debugPerTenantEnabled;
@Value("${mvel.enabled:true}")
private boolean mvelEnabled;
@Value("${tbel.enabled:true}")
private boolean tbelEnabled;
@ApiOperation(value = "Get Rule Chain (getRuleChainById)",
notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
@ -378,13 +378,13 @@ public class RuleChainController extends BaseController {
}
}
@ApiOperation(value = "Is MVEL script executor enabled",
notes = "Returns 'True' if the MVEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH)
@ApiOperation(value = "Is TBEL script executor enabled",
notes = "Returns 'True' if the TBEL script execution is enabled" + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/mvelEnabled", method = RequestMethod.GET)
@RequestMapping(value = "/ruleChain/tbelEnabled", method = RequestMethod.GET)
@ResponseBody
public Boolean isMvelEnabled() {
return mvelEnabled;
public Boolean isTbelEnabled() {
return tbelEnabled;
}
@ApiOperation(value = "Test Script function",
@ -393,7 +393,7 @@ public class RuleChainController extends BaseController {
@RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST)
@ResponseBody
public JsonNode testScript(
@ApiParam(value = "Script language: JS or MVEL")
@ApiParam(value = "Script language: JS or TBEL")
@RequestParam(required = false) ScriptLanguage scriptLang,
@ApiParam(value = "Test JS request. See API call description above.")
@RequestBody JsonNode inputParams) throws ThingsboardException {
@ -418,10 +418,10 @@ public class RuleChainController extends BaseController {
if (ScriptLanguage.JS.equals(scriptLang)) {
engine = new RuleNodeJsScriptEngine(getTenantId(), jsInvokeService, script, argNames);
} else {
if (mvelInvokeService == null) {
throw new IllegalArgumentException("MVEL script engine is disabled!");
if (tbelInvokeService == null) {
throw new IllegalArgumentException("TBEL script engine is disabled!");
}
engine = new RuleNodeMvelScriptEngine(getTenantId(), mvelInvokeService, script, argNames);
engine = new RuleNodeTbelScriptEngine(getTenantId(), tbelInvokeService, script, argNames);
}
TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data);
switch (scriptType) {

6
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java

@ -39,7 +39,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
@ -87,8 +87,8 @@ public class TwoFactorAuthController extends BaseController {
"and Too Many Requests error if rate limits are exceeded.")
@PostMapping("/verification/check")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public JwtTokenPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType,
@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
public JwtPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType,
@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
if (verificationSuccess) {

6
application/src/main/java/org/thingsboard/server/controller/UserController.java

@ -46,7 +46,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.user.TbUserService;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -145,7 +145,7 @@ public class UserController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET)
@ResponseBody
public JwtTokenPair getUserToken(
public JwtPair getUserToken(
@ApiParam(value = USER_ID_PARAM_DESCRIPTION)
@PathVariable(USER_ID) String strUserId) throws ThingsboardException {
checkParameter(USER_ID, strUserId);
@ -182,7 +182,7 @@ public class UserController extends BaseController {
@RequestBody User user,
@ApiParam(value = "Send activation email (or use activation link)", defaultValue = "true")
@RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException {
if (Authority.TENANT_ADMIN.equals(getCurrentUser().getAuthority())) {
if (!Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
user.setTenantId(getCurrentUser().getTenantId());
}
checkEntity(user.getId(), user, Resource.USER);

1
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -273,6 +273,7 @@ public class ThingsboardInstallService {
systemDataLoaderService.createSysAdmin();
systemDataLoaderService.createDefaultTenantProfiles();
systemDataLoaderService.createAdminSettings();
systemDataLoaderService.createRandomJwtSettings();
systemDataLoaderService.loadSystemWidgets();
systemDataLoaderService.createOAuth2Templates();
systemDataLoaderService.createQueues();

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -244,7 +244,7 @@ public final class EdgeGrpcSession implements Closeable {
@Override
public void onFailure(Throwable t) {
String errorMsg = t.getMessage() != null ? t.getMessage() : "";
String errorMsg = EdgeUtils.createErrorMsgFromRootCauseAndStackTrace(t);
UplinkResponseMsg uplinkResponseMsg = UplinkResponseMsg.newBuilder()
.setUplinkMsgId(uplinkMsg.getUplinkMsgId())
.setSuccess(false).setErrorMsg(errorMsg).build();

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

@ -82,6 +82,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
@ -167,6 +168,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired
private QueueService queueService;
@Autowired
private JwtSettingsService jwtSettingsService;
@Bean
protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@ -263,6 +267,16 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings);
}
@Override
public void createRandomJwtSettings() throws Exception {
jwtSettingsService.createRandomJwtSettings();
}
@Override
public void saveLegacyYmlSettings() throws Exception {
jwtSettingsService.saveLegacyYmlSettings();
}
@Override
public void createOAuth2Templates() throws Exception {
installScripts.createOAuth2Templates();
@ -656,4 +670,5 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
queueService.saveQueue(sequentialByOriginatorQueue);
}
}
}

4
application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java

@ -23,6 +23,10 @@ public interface SystemDataLoaderService {
void createAdminSettings() throws Exception;
void createRandomJwtSettings() throws Exception;
void saveLegacyYmlSettings() throws Exception;
void createOAuth2Templates() throws Exception;
void loadSystemWidgets() throws Exception;

1
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -186,6 +186,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
break;
case "3.4.1":
log.info("Updating data from version 3.4.1 to 3.4.2 ...");
systemDataLoaderService.saveLegacyYmlSettings();
boolean skipAuditLogsMigration = getEnv("TB_SKIP_AUDIT_LOGS_MIGRATION", false);
if (!skipAuditLogsMigration) {
log.info("Starting audit logs migration. Can be skipped with TB_SKIP_AUDIT_LOGS_MIGRATION env variable set to true");

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
@ -143,8 +144,9 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
EdgeNotificationService edgeNotificationService,
OtaPackageStateService firmwareStateService,
GitVersionControlQueueService vcQueueService,
PartitionService partitionService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer());
PartitionService partitionService,
Optional<JwtSettingsService> jwtSettingsService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer(), jwtSettingsService);
this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer();

3
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -70,6 +70,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -126,7 +127,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, QueueService queueService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer());
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer(), Optional.empty());
this.statisticsService = statisticsService;
this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory;
this.submitStrategyFactory = submitStrategyFactory;

23
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
@ -76,11 +77,13 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final PartitionService partitionService;
protected final TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer;
protected final Optional<JwtSettingsService> jwtSettingsService;
public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer) {
PartitionService partitionService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer, Optional<JwtSettingsService> jwtSettingsService) {
this.actorContext = actorContext;
this.encodingService = encodingService;
this.tenantProfileCache = tenantProfileCache;
@ -89,6 +92,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
this.apiUsageStateService = apiUsageStateService;
this.partitionService = partitionService;
this.nfConsumer = nfConsumer;
this.jwtSettingsService = jwtSettingsService;
}
public void init(String mainConsumerThreadName, String nfConsumerThreadName) {
@ -172,12 +176,17 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
apiUsageStateService.onTenantProfileUpdate(tenantProfileId);
}
} else if (EntityType.TENANT.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
tenantProfileCache.evict(componentLifecycleMsg.getTenantId());
partitionService.removeTenant(componentLifecycleMsg.getTenantId());
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantUpdate(componentLifecycleMsg.getTenantId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
apiUsageStateService.onTenantDelete((TenantId) componentLifecycleMsg.getEntityId());
if (TenantId.SYS_TENANT_ID.equals(componentLifecycleMsg.getTenantId())) {
jwtSettingsService.ifPresent(JwtSettingsService::reloadJwtSettings);
return;
} else {
tenantProfileCache.evict(componentLifecycleMsg.getTenantId());
partitionService.removeTenant(componentLifecycleMsg.getTenantId());
if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.UPDATED)) {
apiUsageStateService.onTenantUpdate(componentLifecycleMsg.getTenantId());
} else if (componentLifecycleMsg.getEvent().equals(ComponentLifecycleEvent.DELETED)) {
apiUsageStateService.onTenantDelete((TenantId) componentLifecycleMsg.getEntityId());
}
}
} else if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));

6
application/src/main/java/org/thingsboard/server/service/script/RuleNodeMvelScriptEngine.java → application/src/main/java/org/thingsboard/server/service/script/RuleNodeTbelScriptEngine.java

@ -23,7 +23,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.RuleNodeScriptFactory;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.TbMsg;
@ -42,9 +42,9 @@ import java.util.stream.Collectors;
@Slf4j
public class RuleNodeMvelScriptEngine extends RuleNodeScriptEngine<MvelInvokeService, Object> {
public class RuleNodeTbelScriptEngine extends RuleNodeScriptEngine<TbelInvokeService, Object> {
public RuleNodeMvelScriptEngine(TenantId tenantId, MvelInvokeService scriptInvokeService, String script, String... argNames) {
public RuleNodeTbelScriptEngine(TenantId tenantId, TbelInvokeService scriptInvokeService, String script, String... argNames) {
super(tenantId, scriptInvokeService, script, argNames);
}

162
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java

@ -0,0 +1,162 @@
/**
* Copyright © 2016-2022 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.auth.jwt.settings;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class DefaultJwtSettingsService implements JwtSettingsService {
@Lazy
private final AdminSettingsService adminSettingsService;
@Lazy
private final Optional<TbClusterService> tbClusterService;
private final JwtSettingsValidator jwtSettingsValidator;
@Value("${security.jwt.tokenExpirationTime:9000}")
private Integer tokenExpirationTime;
@Value("${security.jwt.refreshTokenExpTime:604800}")
private Integer refreshTokenExpTime;
@Value("${security.jwt.tokenIssuer:thingsboard.io}")
private String tokenIssuer;
@Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}")
private String tokenSigningKey;
private volatile JwtSettings jwtSettings = null; //lazy init
/**
* Create JWT admin settings is intended to be called from Install scripts only
*/
@Override
public void createRandomJwtSettings() {
if (getJwtSettingsFromDb() == null) {
log.info("Creating JWT admin settings...");
this.jwtSettings = getJwtSettingsFromYml();
if (isSigningKeyDefault(jwtSettings)) {
this.jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)));
}
saveJwtSettings(jwtSettings);
} else {
log.info("Skip creating JWT admin settings because they already exist.");
}
}
/**
* Create JWT admin settings is intended to be called from Upgrade scripts only
*/
@Override
public void saveLegacyYmlSettings() {
log.info("Saving legacy JWT admin settings from YML...");
if (getJwtSettingsFromDb() == null) {
saveJwtSettings(getJwtSettingsFromYml());
}
}
@Override
public JwtSettings saveJwtSettings(JwtSettings jwtSettings) {
jwtSettingsValidator.validate(jwtSettings);
final AdminSettings adminJwtSettings = mapJwtToAdminSettings(jwtSettings);
final AdminSettings existedSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY);
if (existedSettings != null) {
adminJwtSettings.setId(existedSettings.getId());
}
log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored");
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings);
tbClusterService.ifPresent(cs -> cs.broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, ComponentLifecycleEvent.UPDATED));
return reloadJwtSettings();
}
@Override
public JwtSettings reloadJwtSettings() {
return getJwtSettings(true);
}
@Override
public JwtSettings getJwtSettings() {
return getJwtSettings(false);
}
public JwtSettings getJwtSettings(boolean forceReload) {
if (this.jwtSettings == null || forceReload) {
synchronized (this) {
if (this.jwtSettings == null || forceReload) {
JwtSettings result = getJwtSettingsFromDb();
if (result == null) {
result = getJwtSettingsFromYml();
log.warn("Loading the JWT settings from YML since there are no settings in DB. Looks like the upgrade script was not applied.");
}
if (isSigningKeyDefault(result)) {
log.warn("WARNING: The platform is configured to use default JWT Signing Key. " +
"This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " +
"Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.");
}
this.jwtSettings = result;
}
}
}
return this.jwtSettings;
}
private JwtSettings getJwtSettingsFromYml() {
return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey);
}
private JwtSettings getJwtSettingsFromDb() {
AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY);
return adminJwtSettings != null ? mapAdminToJwtSettings(adminJwtSettings) : null;
}
private JwtSettings mapAdminToJwtSettings(AdminSettings adminSettings) {
Objects.requireNonNull(adminSettings, "adminSettings for JWT is null");
return JacksonUtil.treeToValue(adminSettings.getJsonValue(), JwtSettings.class);
}
private AdminSettings mapJwtToAdminSettings(JwtSettings jwtSettings) {
Objects.requireNonNull(jwtSettings, "jwtSettings is null");
AdminSettings adminJwtSettings = new AdminSettings();
adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID);
adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY);
adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings));
return adminJwtSettings;
}
private boolean isSigningKeyDefault(JwtSettings settings) {
return TOKEN_SIGNING_KEY_DEFAULT.equals(settings.getTokenSigningKey());
}
}

69
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java

@ -0,0 +1,69 @@
/**
* Copyright © 2016-2022 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.auth.jwt.settings;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.util.Arrays;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.Base64;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Component
@RequiredArgsConstructor
public class DefaultJwtSettingsValidator implements JwtSettingsValidator {
@Override
public void validate(JwtSettings jwtSettings) {
if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) {
throw new DataValidationException("JWT token issuer should be specified!");
}
if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) {
throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!");
}
if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) {
throw new DataValidationException("JWT token expiration time should be at least 1 minute!");
}
if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) {
throw new DataValidationException("JWT token expiration time should greater than JWT refresh token expiration time!");
}
if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) {
throw new DataValidationException("JWT token signing key should be specified!");
}
byte[] decodedKey;
try {
decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey());
} catch (Exception e) {
throw new DataValidationException("JWT token signing key should be a valid Base64 encoded string! " + e.getMessage());
}
if (Arrays.isNullOrEmpty(decodedKey)) {
throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!");
}
if (decodedKey.length * Byte.SIZE < 256 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) {
throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!");
}
System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory
}
}

39
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/InstallJwtSettingsValidator.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2022 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.auth.jwt.settings;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtSettings;
/**
* During Install or upgrade the validation is suppressed to keep existing data
* */
@Primary
@Profile("install")
@Component
@RequiredArgsConstructor
public class InstallJwtSettingsValidator implements JwtSettingsValidator {
@Override
public void validate(JwtSettings jwtSettings) {
}
}

35
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2022 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.auth.jwt.settings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
public interface JwtSettingsService {
String ADMIN_SETTINGS_JWT_KEY = "jwt";
String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey";
JwtSettings getJwtSettings();
JwtSettings reloadJwtSettings();
void createRandomJwtSettings();
void saveLegacyYmlSettings();
JwtSettings saveJwtSettings(JwtSettings jwtSettings);
}

23
application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsValidator.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2022 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.auth.jwt.settings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
public interface JwtSettingsValidator {
void validate(JwtSettings jwtSettings);
}

17
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -104,10 +104,10 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
registration);
JwtTokenPair tokenPair = tokenFactory.createTokenPair(securityUser);
clearAuthenticationAttributes(request, response);
getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken());
JwtPair tokenPair = tokenFactory.createTokenPair(securityUser);
getRedirectStrategy().sendRedirect(request, response, getRedirectUrl(baseUrl, tokenPair));
systemSecurityService.logLoginAction(securityUser, new RestAuthenticationDetails(request), ActionType.LOGIN, registration.getName(), null);
} catch (Exception e) {
log.debug("Error occurred during processing authentication success result. " +
@ -128,4 +128,13 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
super.clearAuthenticationAttributes(request);
httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}
String getRedirectUrl(String baseUrl, JwtPair tokenPair) {
if (baseUrl.indexOf("?") > 0) {
baseUrl += "&";
} else {
baseUrl += "/?";
}
return baseUrl + "accessToken=" + tokenPair.getToken() + "&refreshToken=" + tokenPair.getRefreshToken();
}
}

4
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java

@ -26,7 +26,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -49,7 +49,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
JwtTokenPair tokenPair = new JwtTokenPair();
JwtPair tokenPair = new JwtPair();
if (authentication instanceof MfaAuthenticationToken) {
int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true)

32
application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java

@ -24,8 +24,8 @@ import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
@ -35,9 +35,9 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -49,6 +49,7 @@ import java.util.UUID;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtTokenFactory {
@ -62,12 +63,7 @@ public class JwtTokenFactory {
private static final String CUSTOMER_ID = "customerId";
private static final String SESSION_ID = "sessionId";
private final JwtSettings settings;
@Autowired
public JwtTokenFactory(JwtSettings settings) {
this.settings = settings;
}
private final JwtSettingsService jwtSettingsService;
/**
* Factory method for issuing new JWT Tokens.
@ -80,7 +76,7 @@ public class JwtTokenFactory {
UserPrincipal principal = securityUser.getUserPrincipal();
JwtBuilder jwtBuilder = setUpToken(securityUser, securityUser.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()), settings.getTokenExpirationTime());
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()), jwtSettingsService.getJwtSettings().getTokenExpirationTime());
jwtBuilder.claim(FIRST_NAME, securityUser.getFirstName())
.claim(LAST_NAME, securityUser.getLastName())
.claim(ENABLED, securityUser.isEnabled())
@ -142,7 +138,7 @@ public class JwtTokenFactory {
public JwtToken createRefreshToken(SecurityUser securityUser) {
UserPrincipal principal = securityUser.getUserPrincipal();
String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), settings.getRefreshTokenExpTime())
String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), jwtSettingsService.getJwtSettings().getRefreshTokenExpTime())
.claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID)
.setId(UUID.randomUUID().toString()).compact();
@ -198,30 +194,30 @@ public class JwtTokenFactory {
return Jwts.builder()
.setClaims(claims)
.setIssuer(settings.getTokenIssuer())
.setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer())
.setIssuedAt(Date.from(currentTime.toInstant()))
.setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey());
.signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey());
}
public Jws<Claims> parseTokenClaims(JwtToken token) {
try {
return Jwts.parser()
.setSigningKey(settings.getTokenSigningKey())
.setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey())
.parseClaimsJws(token.getToken());
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) {
} catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) {
log.debug("Invalid JWT Token", ex);
throw new BadCredentialsException("Invalid JWT token: ", ex);
} catch (ExpiredJwtException expiredEx) {
} catch (SignatureException | ExpiredJwtException expiredEx) {
log.debug("JWT Token is expired", expiredEx);
throw new JwtExpiredTokenException(token, "JWT Token expired", expiredEx);
}
}
public JwtTokenPair createTokenPair(SecurityUser securityUser) {
public JwtPair createTokenPair(SecurityUser securityUser) {
JwtToken accessToken = createAccessJwtToken(securityUser);
JwtToken refreshToken = createRefreshToken(securityUser);
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
return new JwtPair(accessToken.getToken(), refreshToken.getToken());
}
}

38
application/src/main/resources/thingsboard.yml

@ -107,11 +107,11 @@ plugins:
# Security parameters
security:
# JWT Token parameters
jwt:
jwt: # Since 3.4.2 values are persisted to the database during install or upgrade. On Install, the key will be generated randomly if no custom value set. You can change it later from Web UI under SYS_ADMIN
tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:9000}" # Number of seconds (2.5 hours)
refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:604800}" # Number of seconds (1 week).
tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}"
tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" # Base64 encoded
# Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator
user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}"
# Enable/disable case-sensitive username login
@ -148,7 +148,7 @@ ui:
# Help parameters
help:
# Base url for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.4.1}"
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.4.2}"
database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records
@ -227,6 +227,7 @@ cassandra:
default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
# Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
use_ts_key_value_partitioning_on_read: "${USE_TS_KV_PARTITIONING_ON_READ:true}"
ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}"
ts_key_value_ttl: "${TS_KV_TTL:0}"
buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
@ -610,24 +611,25 @@ state:
defaultStateCheckIntervalInSec: "${DEFAULT_STATE_CHECK_INTERVAL:60}"
persistToTelemetry: "${PERSIST_STATE_TO_TELEMETRY:false}"
mvel:
enabled: "${MVEL_ENABLED:true}"
max_total_args_size: "${MVEL_MAX_TOTAL_ARGS_SIZE:100000}"
max_result_size: "${MVEL_MAX_RESULT_SIZE:300000}"
max_script_body_size: "${MVEL_MAX_SCRIPT_BODY_SIZE:50000}"
# Maximum allowed MVEL script execution memory
max_memory_limit_mb: "${MVEL_MAX_MEMORY_LIMIT_MB: 8}"
# Maximum allowed MVEL script execution errors before it will be blacklisted
max_errors: "${MVEL_MAX_ERRORS:3}"
# MVEL Eval max request timeout in milliseconds. 0 - no timeout
max_requests_timeout: "${MVEL_MAX_REQUEST_TIMEOUT:500}"
tbel:
enabled: "${TBEL_ENABLED:true}"
max_total_args_size: "${TBEL_MAX_TOTAL_ARGS_SIZE:100000}"
max_result_size: "${TBEL_MAX_RESULT_SIZE:300000}"
max_script_body_size: "${TBEL_MAX_SCRIPT_BODY_SIZE:50000}"
# Maximum allowed TBEL script execution memory
max_memory_limit_mb: "${TBEL_MAX_MEMORY_LIMIT_MB: 8}"
# Maximum allowed TBEL script execution errors before it will be blacklisted
max_errors: "${TBEL_MAX_ERRORS:3}"
# TBEL Eval max request timeout in milliseconds. 0 - no timeout
max_requests_timeout: "${TBEL_MAX_REQUEST_TIMEOUT:500}"
# Maximum time in seconds for black listed function to stay in the list.
max_black_list_duration_sec: "${MVEL_MAX_BLACKLIST_DURATION_SEC:60}"
max_black_list_duration_sec: "${TBEL_MAX_BLACKLIST_DURATION_SEC:60}"
# Specify thread pool size for javascript executor service
thread_pool_size: "${MVEL_THREAD_POOL_SIZE:50}"
thread_pool_size: "${TBEL_THREAD_POOL_SIZE:50}"
compiled_scripts_cache_size: "${TBEL_COMPILED_SCRIPTS_CACHE_SIZE:1000}"
stats:
enabled: "${TB_MVEL_STATS_ENABLED:false}"
print_interval_ms: "${TB_MVEL_STATS_PRINT_INTERVAL_MS:10000}"
enabled: "${TB_TBEL_STATS_ENABLED:false}"
print_interval_ms: "${TB_TBEL_STATS_PRINT_INTERVAL_MS:10000}"
js:
evaluator: "${JS_EVALUATOR:local}" # local/remote

122
application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java

@ -17,14 +17,21 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.mail.DefaultMailService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
@ -32,8 +39,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
public abstract class BaseAdminControllerTest extends AbstractControllerTest {
final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey");
@Autowired
MailService mailService;
@ -45,67 +53,67 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
public void testFindAdminSettingsByKey() throws Exception {
loginSysAdmin();
doGet("/api/admin/settings/general")
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.key", is("general")))
.andExpect(jsonPath("$.jsonValue.baseUrl", is("http://localhost:8080")));
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.key", is("general")))
.andExpect(jsonPath("$.jsonValue.baseUrl", is("http://localhost:8080")));
doGet("/api/admin/settings/mail")
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.key", is("mail")))
.andExpect(jsonPath("$.jsonValue.smtpProtocol", is("smtp")))
.andExpect(jsonPath("$.jsonValue.smtpHost", is("localhost")))
.andExpect(jsonPath("$.jsonValue.smtpPort", is("25")));
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.id", notNullValue()))
.andExpect(jsonPath("$.key", is("mail")))
.andExpect(jsonPath("$.jsonValue.smtpProtocol", is("smtp")))
.andExpect(jsonPath("$.jsonValue.smtpHost", is("localhost")))
.andExpect(jsonPath("$.jsonValue.smtpPort", is("25")));
doGet("/api/admin/settings/unknown")
.andExpect(status().isNotFound());
.andExpect(status().isNotFound());
}
@Test
public void testSaveAdminSettings() throws Exception {
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/general", AdminSettings.class);
AdminSettings adminSettings = doGet("/api/admin/settings/general", AdminSettings.class);
JsonNode jsonValue = adminSettings.getJsonValue();
((ObjectNode) jsonValue).put("baseUrl", "http://myhost.org");
adminSettings.setJsonValue(jsonValue);
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
doGet("/api/admin/settings/general")
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.jsonValue.baseUrl", is("http://myhost.org")));
.andExpect(status().isOk())
.andExpect(content().contentType(contentType))
.andExpect(jsonPath("$.jsonValue.baseUrl", is("http://myhost.org")));
((ObjectNode) jsonValue).put("baseUrl", "http://localhost:8080");
adminSettings.setJsonValue(jsonValue);
doPost("/api/admin/settings", adminSettings)
.andExpect(status().isOk());
.andExpect(status().isOk());
}
@Test
public void testSaveAdminSettingsWithEmptyKey() throws Exception {
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
adminSettings.setKey(null);
doPost("/api/admin/settings", adminSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Key should be specified")));
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Key should be specified")));
}
@Test
public void testChangeAdminSettingsKey() throws Exception {
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
adminSettings.setKey("newKey");
doPost("/api/admin/settings", adminSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("is prohibited")));
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("is prohibited")));
}
@Test
@ -113,7 +121,7 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
doPost("/api/admin/settings/testMail", adminSettings)
.andExpect(status().isOk());
.andExpect(status().isOk());
}
@Test
@ -139,4 +147,48 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
doPost("/api/admin/settings/testMail", adminSettings).andExpect(status().is5xxServerError());
Mockito.doNothing().when(mailService).sendTestMail(Mockito.any(), Mockito.any());
}
void resetJwtSettingsToDefault() throws Exception {
loginSysAdmin();
doPost("/api/admin/jwtSettings", defaultJwtSettings).andExpect(status().isOk()); // jwt test scenarios are always started from
loginTenantAdmin();
}
@Test
public void testGetAndSaveDefaultJwtSettings() throws Exception {
JwtSettings jwtSettings;
loginSysAdmin();
jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk());
jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
resetJwtSettingsToDefault();
}
@Test
public void testCreateJwtSettings() throws Exception {
loginSysAdmin();
JwtSettings jwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(defaultJwtSettings);
jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(
RandomStringUtils.randomAlphanumeric(256 / Byte.SIZE).getBytes(StandardCharsets.UTF_8)));
doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk());
doGet("/api/admin/jwtSettings").andExpect(status().isUnauthorized()); //the old JWT token does not work after signing key was changed!
loginSysAdmin();
JwtSettings newJwtSettings = doGet("/api/admin/jwtSettings", JwtSettings.class);
assertThat(jwtSettings).isEqualTo(newJwtSettings);
resetJwtSettingsToDefault();
}
}

4
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java

@ -51,7 +51,7 @@ import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.common.data.security.model.JwtPair;
import java.time.Duration;
import java.util.Arrays;
@ -396,7 +396,7 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
private void logInWithPreVerificationToken(String username, String password) throws Exception {
LoginRequest loginRequest = new LoginRequest(username, password);
JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class);
JwtPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtPair.class);
assertThat(response.getToken()).isNotNull();
assertThat(response.getRefreshToken()).isNull();
assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN);

114
application/src/test/java/org/thingsboard/server/service/script/MvelInvokeServiceTest.java → application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.script;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.benmanes.caffeine.cache.Cache;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@ -23,31 +24,39 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.ScriptType;
import org.thingsboard.script.api.mvel.MvelInvokeService;
import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.script.api.tbel.TbelScript;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DaoSqlTest
@TestPropertySource(properties = {
"mvel.max_script_body_size=100",
"mvel.max_total_args_size=50",
"mvel.max_result_size=50",
"mvel.max_errors=2",
"tbel.max_script_body_size=100",
"tbel.max_total_args_size=50",
"tbel.max_result_size=50",
"tbel.max_errors=2",
"tbel.compiled_scripts_cache_size=100"
})
class MvelInvokeServiceTest extends AbstractControllerTest {
class TbelInvokeServiceTest extends AbstractControllerTest {
@Autowired
private MvelInvokeService invokeService;
private TbelInvokeService invokeService;
@Value("${mvel.max_errors}")
@Value("${tbel.max_errors}")
private int maxJsErrors;
@Test
@ -110,6 +119,89 @@ class MvelInvokeServiceTest extends AbstractControllerTest {
assertThatScriptIsBlocked(scriptId);
}
@Test
void givenScriptsWithSameBody_thenCompileAndCacheOnlyOnce() throws Exception {
String script = "return msg.temperature > 20;";
List<UUID> scriptsIds = new ArrayList<>();
for (int i = 0; i < 100; i++) {
UUID scriptId = evalScript(script);
scriptsIds.add(scriptId);
}
Map<UUID, String> scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash");
Map<String, TbelScript> scriptMap = getFieldValue(invokeService, "scriptMap");
Cache<String, Serializable> compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache");
String scriptHash = scriptIdToHash.get(scriptsIds.get(0));
assertThat(scriptsIds.stream().map(scriptIdToHash::get)).containsOnly(scriptHash);
assertThat(scriptMap).containsKey(scriptHash);
assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNotNull();
}
@Test
public void whenReleasingScript_thenCheckForScriptHashUsages() throws Exception {
String script = "return msg.temperature > 20;";
List<UUID> scriptsIds = new ArrayList<>();
for (int i = 0; i < 10; i++) {
UUID scriptId = evalScript(script);
scriptsIds.add(scriptId);
}
Map<UUID, String> scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash");
Map<String, TbelScript> scriptMap = getFieldValue(invokeService, "scriptMap");
Cache<String, Serializable> compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache");
String scriptHash = scriptIdToHash.get(scriptsIds.get(0));
for (int i = 0; i < 9; i++) {
UUID scriptId = scriptsIds.get(i);
assertThat(scriptIdToHash).containsKey(scriptId);
invokeService.release(scriptId);
assertThat(scriptIdToHash).doesNotContainKey(scriptId);
}
assertThat(scriptMap).containsKey(scriptHash);
assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNotNull();
invokeService.release(scriptsIds.get(9));
assertThat(scriptMap).doesNotContainKey(scriptHash);
assertThat(compiledScriptsCache.getIfPresent(scriptHash)).isNull();
}
@Test
public void whenCompiledScriptsCacheIsTooBig_thenRemoveRarelyUsedScripts() throws Exception {
Map<UUID, String> scriptIdToHash = getFieldValue(invokeService, "scriptIdToHash");
Cache<String, Serializable> compiledScriptsCache = getFieldValue(invokeService, "compiledScriptsCache");
List<UUID> scriptsIds = new ArrayList<>();
for (int i = 0; i < 110; i++) { // tbel.compiled_scripts_cache_size = 100
String script = "return msg.temperature > " + i;
UUID scriptId = evalScript(script);
scriptsIds.add(scriptId);
for (int j = 0; j < i; j++) {
invokeScript(scriptId, "{ \"temperature\": 12 }"); // so that scriptsIds is ordered by number of invocations
}
}
ConcurrentMap<String, Serializable> cache = compiledScriptsCache.asMap();
for (int i = 0; i < 10; i++) { // iterating rarely used scripts
UUID scriptId = scriptsIds.get(i);
String scriptHash = scriptIdToHash.get(scriptId);
assertThat(cache).doesNotContainKey(scriptHash);
}
for (int i = 10; i < 110; i++) {
UUID scriptId = scriptsIds.get(i);
String scriptHash = scriptIdToHash.get(scriptId);
assertThat(cache).containsKey(scriptHash);
}
UUID scriptRemovedFromCache = scriptsIds.get(0);
assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNull();
invokeScript(scriptRemovedFromCache, "{ \"temperature\": 12 }");
assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNotNull();
}
private void assertThatScriptIsBlocked(UUID scriptId) {
assertThatThrownBy(() -> {
invokeScript(scriptId, "{}");
@ -125,4 +217,10 @@ class MvelInvokeServiceTest extends AbstractControllerTest {
return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get().toString();
}
private <T> T getFieldValue(Object target, String fieldName) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return (T) field.get(target);
}
}

10
application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java

@ -23,7 +23,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.AccessJwtToken;
@ -36,6 +37,8 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
public class JwtTokenFactoryTest {
@ -50,7 +53,10 @@ public class JwtTokenFactoryTest {
jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2));
jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7));
tokenFactory = new JwtTokenFactory(jwtSettings);
JwtSettingsService jwtSettingsService = mock(JwtSettingsService.class);
willReturn(jwtSettings).given(jwtSettingsService).getJwtSettings();
tokenFactory = new JwtTokenFactory(jwtSettingsService);
}
@Test

68
application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandlerTest.java

@ -0,0 +1,68 @@
/**
* Copyright © 2016-2022 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.auth.oauth2;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@DaoSqlTest
public class Oauth2AuthenticationSuccessHandlerTest extends AbstractControllerTest {
@Autowired
private Oauth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler;
@Mock
private JwtTokenFactory jwtTokenFactory;
private SecurityUser securityUser;
@Before
public void before() {
UserId userId = new UserId(UUID.randomUUID());
securityUser = new SecurityUser(userId);
when(jwtTokenFactory.createTokenPair(eq(securityUser))).thenReturn(new JwtPair("testAccessToken", "testRefreshToken"));
}
@Test
public void testGetRedirectUrl() {
JwtPair jwtPair = jwtTokenFactory.createTokenPair(securityUser);
String urlWithoutParams = "http://localhost:8080/dashboardGroups/3fa13530-6597-11ed-bd76-8bd591f0ec3e";
String urlWithParams = "http://localhost:8080/dashboardGroups/3fa13530-6597-11ed-bd76-8bd591f0ec3e?state=someState&page=1";
String redirectUrl = oauth2AuthenticationSuccessHandler.getRedirectUrl(urlWithoutParams, jwtPair);
String expectedUrl = urlWithoutParams + "/?accessToken=" + jwtPair.getToken() + "&refreshToken=" + jwtPair.getRefreshToken();
assertEquals(expectedUrl, redirectUrl);
redirectUrl = oauth2AuthenticationSuccessHandler.getRedirectUrl(urlWithParams, jwtPair);
expectedUrl = urlWithParams + "&accessToken=" + jwtPair.getToken() + "&refreshToken=" + jwtPair.getRefreshToken();
assertEquals(expectedUrl, redirectUrl);
}
}

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

@ -16,6 +16,8 @@
<logger name="org.cassandraunit" level="INFO"/>
<logger name="org.eclipse.leshan" level="INFO"/>
<!-- mute TelemetryEdgeSqlTest that causes a lot of randomly generated errors -->
<logger name="org.thingsboard.server.service.edge.rpc.EdgeGrpcSession" level="OFF"/>
<root level="WARN">
<appender-ref ref="console"/>

19
common/data/src/main/java/org/thingsboard/server/common/data/EdgeUtils.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Throwables;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
@ -29,6 +30,8 @@ import java.util.concurrent.ThreadLocalRandom;
@Slf4j
public final class EdgeUtils {
private static final int STACK_TRACE_LIMIT = 10;
private EdgeUtils() {
}
@ -93,4 +96,20 @@ public final class EdgeUtils {
edgeEvent.setBody(body);
return edgeEvent;
}
public static String createErrorMsgFromRootCauseAndStackTrace(Throwable t) {
Throwable rootCause = Throwables.getRootCause(t);
StringBuilder errorMsg = new StringBuilder(rootCause.getMessage() != null ? rootCause.getMessage() : "");
if (rootCause.getStackTrace().length > 0) {
int idx = 0;
for (StackTraceElement stackTraceElement : rootCause.getStackTrace()) {
errorMsg.append("\n").append(stackTraceElement.toString());
idx++;
if (idx > STACK_TRACE_LIMIT) {
break;
}
}
}
return errorMsg.toString();
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBasedWithAdditionalInfo.java

@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -36,6 +37,7 @@ import java.util.function.Consumer;
public abstract class SearchTextBasedWithAdditionalInfo<I extends UUIDBased> extends SearchTextBased<I> implements HasAdditionalInfo {
public static final ObjectMapper mapper = new ObjectMapper();
@NoXss
private transient JsonNode additionalInfo;
@JsonIgnore
private byte[] additionalInfoBytes;

2
common/data/src/main/java/org/thingsboard/server/common/data/script/ScriptLanguage.java

@ -16,5 +16,5 @@
package org.thingsboard.server.common.data.script;
public enum ScriptLanguage {
JS, MVEL
JS, TBEL
}

9
application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtPair.java

@ -13,19 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.security.model;
package org.thingsboard.server.common.data.security.model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.security.Authority;
@ApiModel(value = "JWT Token Pair")
@ApiModel(value = "JWT Pair")
@Data
@NoArgsConstructor
public class JwtTokenPair {
public class JwtPair {
@ApiModelProperty(position = 1, value = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..")
private String token;
@ -34,7 +33,7 @@ public class JwtTokenPair {
private Authority scope;
public JwtTokenPair(String token, String refreshToken) {
public JwtPair(String token, String refreshToken) {
this.token = token;
this.refreshToken = refreshToken;
}

30
application/src/main/java/org/thingsboard/server/config/JwtSettings.java → common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java

@ -13,35 +13,43 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.config;
package org.thingsboard.server.common.data.security.model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtToken;
import lombok.NoArgsConstructor;
@Component
@ConfigurationProperties(prefix = "security.jwt")
@ApiModel(value = "JWT Settings")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class JwtSettings {
/**
* {@link JwtToken} will expire after this time.
*/
@ApiModelProperty(position = 1, value = "The JWT will expire after seconds.", example = "9000")
private Integer tokenExpirationTime;
/**
* {@link JwtToken} can be refreshed during this timeframe.
*/
@ApiModelProperty(position = 2, value = "The JWT can be refreshed during seconds.", example = "604800")
private Integer refreshTokenExpTime;
/**
* Token issuer.
*/
@ApiModelProperty(position = 3, value = "The JWT issuer.", example = "thingsboard.io")
private String tokenIssuer;
/**
* Key is used to sign {@link JwtToken}.
* Base64 encoded
*/
@ApiModelProperty(position = 4, value = "The JWT key is used to sing token. Base64 encoded.", example = "cTU4WnNqemI2aU5wbWVjdm1vYXRzanhjNHRUcXliMjE=")
private String tokenSigningKey;
/**
* {@link JwtToken} can be refreshed during this timeframe.
*/
private Integer refreshTokenExpTime;
}

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

@ -56,6 +56,10 @@
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
@ -86,7 +90,7 @@
</dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>mvel2</artifactId>
<artifactId>tbel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

112
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/DefaultMvelInvokeService.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DefaultTbelInvokeService.java

@ -13,8 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
package org.thingsboard.script.api.tbel;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
@ -42,63 +46,74 @@ import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Pattern;
@Slf4j
@ConditionalOnProperty(prefix = "mvel", value = "enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnProperty(prefix = "tbel", value = "enabled", havingValue = "true", matchIfMissing = true)
@Service
public class DefaultMvelInvokeService extends AbstractScriptInvokeService implements MvelInvokeService {
public class DefaultTbelInvokeService extends AbstractScriptInvokeService implements TbelInvokeService {
protected final Map<UUID, String> scriptIdToHash = new ConcurrentHashMap<>();
protected final Map<String, TbelScript> scriptMap = new ConcurrentHashMap<>();
protected Cache<String, Serializable> compiledScriptsCache;
protected Map<UUID, MvelScript> scriptMap = new ConcurrentHashMap<>();
private SandboxedParserConfiguration parserConfig;
private static final Pattern NEW_KEYWORD_PATTERN = Pattern.compile("new\\s");
@Getter
@Value("${mvel.max_total_args_size:100000}")
@Value("${tbel.max_total_args_size:100000}")
private long maxTotalArgsSize;
@Getter
@Value("${mvel.max_result_size:300000}")
@Value("${tbel.max_result_size:300000}")
private long maxResultSize;
@Getter
@Value("${mvel.max_script_body_size:50000}")
@Value("${tbel.max_script_body_size:50000}")
private long maxScriptBodySize;
@Getter
@Value("${mvel.max_errors:3}")
@Value("${tbel.max_errors:3}")
private int maxErrors;
@Getter
@Value("${mvel.max_black_list_duration_sec:60}")
@Value("${tbel.max_black_list_duration_sec:60}")
private int maxBlackListDurationSec;
@Getter
@Value("${mvel.max_requests_timeout:0}")
@Value("${tbel.max_requests_timeout:0}")
private long maxInvokeRequestsTimeout;
@Getter
@Value("${mvel.stats.enabled:false}")
@Value("${tbel.stats.enabled:false}")
private boolean statsEnabled;
@Value("${mvel.thread_pool_size:50}")
@Value("${tbel.thread_pool_size:50}")
private int threadPoolSize;
@Value("${mvel.max_memory_limit_mb:8}")
@Value("${tbel.max_memory_limit_mb:8}")
private long maxMemoryLimitMb;
@Value("${tbel.compiled_scripts_cache_size:1000}")
private int compiledScriptsCacheSize;
private ListeningExecutorService executor;
protected DefaultMvelInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) {
private final Lock lock = new ReentrantLock();
protected DefaultTbelInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) {
super(apiUsageStateClient, apiUsageReportClient);
}
@Scheduled(fixedDelayString = "${mvel.stats.print_interval_ms:10000}")
@Scheduled(fixedDelayString = "${tbel.stats.print_interval_ms:10000}")
public void printStats() {
super.printStats();
}
@ -112,14 +127,17 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
parserConfig.addImport("JSON", TbJson.class);
parserConfig.registerDataType("Date", TbDate.class, date -> 8L);
TbUtils.register(parserConfig);
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "mvel-executor"));
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));
try {
// Special command to warm up MVEL engine
Serializable script = MVEL.compileExpression("var warmUp = {}; warmUp", new SandboxedParserContext(parserConfig));
// Special command to warm up TBEL engine
Serializable script = compileScript("var warmUp = {}; warmUp");
MVEL.executeTbExpression(script, new ExecutionContext(parserConfig), Collections.emptyMap());
} catch (Exception e) {
// do nothing
}
compiledScriptsCache = Caffeine.newBuilder()
.maximumSize(compiledScriptsCacheSize)
.build();
}
@PreDestroy
@ -131,7 +149,7 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
@Override
protected String getStatsName() {
return "MVEL Scripts Stats";
return "TBEL Scripts Stats";
}
@Override
@ -141,16 +159,22 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
@Override
protected boolean isScriptPresent(UUID scriptId) {
return scriptMap.containsKey(scriptId);
return scriptIdToHash.containsKey(scriptId);
}
@Override
protected ListenableFuture<UUID> doEvalScript(TenantId tenantId, ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) {
return executor.submit(() -> {
try {
Serializable compiledScript = MVEL.compileExpression(scriptBody, new SandboxedParserContext(parserConfig));
MvelScript script = new MvelScript(compiledScript, scriptBody, argNames);
scriptMap.put(scriptId, script);
String scriptHash = hash(scriptBody, argNames);
compiledScriptsCache.get(scriptHash, k -> compileScript(scriptBody));
lock.lock();
try {
scriptIdToHash.put(scriptId, scriptHash);
scriptMap.computeIfAbsent(scriptHash, k -> new TbelScript(scriptBody, argNames));
} finally {
lock.unlock();
}
return scriptId;
} catch (Exception e) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.COMPILATION, scriptBody, e);
@ -159,15 +183,17 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
}
@Override
protected MvelScriptExecutionTask doInvokeFunction(UUID scriptId, Object[] args) {
protected TbelScriptExecutionTask doInvokeFunction(UUID scriptId, Object[] args) {
ExecutionContext executionContext = new ExecutionContext(this.parserConfig, maxMemoryLimitMb * 1024 * 1024);
return new MvelScriptExecutionTask(executionContext, executor.submit(() -> {
MvelScript script = scriptMap.get(scriptId);
if (script == null) {
return new TbelScriptExecutionTask(executionContext, executor.submit(() -> {
String scriptHash = scriptIdToHash.get(scriptId);
if (scriptHash == null) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, null, new RuntimeException("Script not found!"));
}
TbelScript script = scriptMap.get(scriptHash);
Serializable compiledScript = compiledScriptsCache.get(scriptHash, k -> compileScript(script.getScriptBody()));
try {
return MVEL.executeTbExpression(script.getCompiledScript(), executionContext, script.createVars(args));
return MVEL.executeTbExpression(compiledScript, executionContext, script.createVars(args));
} catch (ScriptMemoryOverflowException e) {
throw new TbScriptException(scriptId, TbScriptException.ErrorCode.OTHER, script.getScriptBody(), new RuntimeException("Script memory overflow!"));
} catch (Exception e) {
@ -177,7 +203,33 @@ public class DefaultMvelInvokeService extends AbstractScriptInvokeService implem
}
@Override
protected void doRelease(UUID scriptId) throws Exception {
scriptMap.remove(scriptId);
protected void doRelease(UUID scriptId) {
String scriptHash = scriptIdToHash.remove(scriptId);
if (scriptHash != null) {
lock.lock();
try {
if (!scriptIdToHash.containsValue(scriptHash)) {
scriptMap.remove(scriptHash);
compiledScriptsCache.invalidate(scriptHash);
}
} finally {
lock.unlock();
}
}
}
private Serializable compileScript(String scriptBody) {
return MVEL.compileExpression(scriptBody, new SandboxedParserContext(parserConfig));
}
@SuppressWarnings("UnstableApiUsage")
protected String hash(String scriptBody, String[] argNames) {
Hasher hasher = Hashing.murmur3_128().newHasher();
hasher.putUnencodedChars(scriptBody);
for (String argName : argNames) {
hasher.putString(argName, StandardCharsets.UTF_8);
}
return hasher.hash().toString();
}
}

4
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbDate.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
package org.thingsboard.script.api.tbel;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@ -60,7 +60,7 @@ public class TbDate extends Date {
}
public String toISOString() {
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sssZ");
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
return formatter.format(this);
}

2
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbJson.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbJson.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
package org.thingsboard.script.api.tbel;
import com.fasterxml.jackson.databind.JsonNode;
import org.mvel2.ExecutionContext;

104
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/TbUtils.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -13,19 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
package org.thingsboard.script.api.tbel;
import org.mvel2.ExecutionContext;
import org.mvel2.ParserConfiguration;
import org.mvel2.execution.ExecutionArrayList;
import org.mvel2.util.MethodStub;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
public class TbUtils {
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
public static void register(ParserConfiguration parserConfig) throws Exception {
parserConfig.addImport("btoa", new MethodStub(TbUtils.class.getMethod("btoa",
String.class)));
@ -35,6 +41,10 @@ public class TbUtils {
List.class)));
parserConfig.addImport("bytesToString", new MethodStub(TbUtils.class.getMethod("bytesToString",
List.class, String.class)));
parserConfig.addImport("decodeToString", new MethodStub(TbUtils.class.getMethod("bytesToString",
List.class)));
parserConfig.addImport("decodeToJson", new MethodStub(TbUtils.class.getMethod("decodeToJson",
ExecutionContext.class, List.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
ExecutionContext.class, String.class)));
parserConfig.addImport("stringToBytes", new MethodStub(TbUtils.class.getMethod("stringToBytes",
@ -47,10 +57,24 @@ public class TbUtils {
String.class)));
parserConfig.addImport("parseDouble", new MethodStub(TbUtils.class.getMethod("parseDouble",
String.class)));
}
public static void main(String[] args) {
System.out.println(Integer.class == int.class);
parserConfig.addImport("parseLittleEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseLittleEndianHexToInt",
String.class)));
parserConfig.addImport("parseBigEndianHexToInt", new MethodStub(TbUtils.class.getMethod("parseBigEndianHexToInt",
String.class)));
parserConfig.addImport("parseHexToInt", new MethodStub(TbUtils.class.getMethod("parseHexToInt",
String.class)));
parserConfig.addImport("parseHexToInt", new MethodStub(TbUtils.class.getMethod("parseHexToInt",
String.class, boolean.class)));
parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed",
double.class, int.class)));
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
ExecutionContext.class, String.class)));
parserConfig.addImport("base64ToHex", new MethodStub(TbUtils.class.getMethod("base64ToHex",
String.class)));
parserConfig.addImport("bytesToHex", new MethodStub(TbUtils.class.getMethod("bytesToHex",
byte[].class)));
parserConfig.addImport("bytesToHex", new MethodStub(TbUtils.class.getMethod("bytesToHex",
ExecutionArrayList.class)));
}
public static String btoa(String input) {
@ -61,6 +85,10 @@ public class TbUtils {
return new String(Base64.getDecoder().decode(encoded));
}
public static Object decodeToJson(ExecutionContext ctx, List<Byte> bytesList) throws IOException {
return TbJson.parse(ctx, bytesToString(bytesList));
}
public static String bytesToString(List<Byte> bytesList) {
byte[] bytes = bytesFromList(bytesList);
return new String(bytes);
@ -149,6 +177,72 @@ public class TbUtils {
return null;
}
public static int parseLittleEndianHexToInt(String hex) {
return parseHexToInt(hex, false);
}
public static int parseBigEndianHexToInt(String hex) {
return parseHexToInt(hex, true);
}
public static int parseHexToInt(String hex) {
return parseHexToInt(hex, true);
}
public static int parseHexToInt(String hex, boolean bigEndian) {
int length = hex.length();
if (length > 8) {
throw new IllegalArgumentException("Hex string is too large. Maximum 8 symbols allowed.");
}
if (bigEndian) {
return Integer.parseInt(hex, 16);
} else {
if (length < 8) {
hex = hex + "0".repeat(8 - length);
}
return Integer.reverseBytes(Integer.parseInt(hex, 16));
}
}
public static ExecutionArrayList<Integer> hexToBytes(ExecutionContext ctx, String hex) {
int len = hex.length();
if (len % 2 > 0) {
throw new IllegalArgumentException("Hex string must be even-length.");
}
ExecutionArrayList<Integer> data = new ExecutionArrayList<>(ctx);
for (int i = 0; i < len; i += 2) {
data.add((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
public static String base64ToHex(String base64) {
return bytesToHex(Base64.getDecoder().decode(base64));
}
public static String bytesToHex(ExecutionArrayList<?> bytesList) {
byte[] bytes = new byte[bytesList.size()];
for (int i = 0; i < bytesList.size(); i++) {
bytes[i] = Byte.parseByte(bytesList.get(i).toString());
}
return bytesToHex(bytes);
}
public static String bytesToHex(byte[] bytes) {
byte[] hexChars = new byte[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars, StandardCharsets.UTF_8);
}
public static double toFixed(double value, int precision) {
return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).doubleValue();
}
private static boolean isHexadecimal(String value) {
return value != null && (value.contains("0x") || value.contains("0X"));
}

6
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelInvokeService.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelInvokeService.java

@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
package org.thingsboard.script.api.tbel;
import org.thingsboard.script.api.ScriptInvokeService;
import org.thingsboard.server.common.data.script.ScriptLanguage;
public interface MvelInvokeService extends ScriptInvokeService {
public interface TbelInvokeService extends ScriptInvokeService {
@Override
default ScriptLanguage getLanguage() {
return ScriptLanguage.MVEL;
return ScriptLanguage.TBEL;
}
}

6
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScript.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelScript.java

@ -13,18 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
package org.thingsboard.script.api.tbel;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@Data
public class MvelScript {
public class TbelScript {
private final Serializable compiledScript;
private final String scriptBody;
private final String[] argNames;

8
common/script/script-api/src/main/java/org/thingsboard/script/api/mvel/MvelScriptExecutionTask.java → common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelScriptExecutionTask.java

@ -13,20 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.script.api.mvel;
package org.thingsboard.script.api.tbel;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data;
import lombok.Getter;
import org.mvel2.ExecutionContext;
import org.thingsboard.script.api.TbScriptExecutionTask;
public class MvelScriptExecutionTask extends TbScriptExecutionTask {
public class TbelScriptExecutionTask extends TbScriptExecutionTask {
private final ExecutionContext context;
public MvelScriptExecutionTask(ExecutionContext context, ListenableFuture<Object> resultFuture) {
public TbelScriptExecutionTask(ExecutionContext context, ListenableFuture<Object> resultFuture) {
super(resultFuture);
this.context = context;
}

11
dao/src/main/java/org/thingsboard/server/dao/service/NoXssValidator.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.service;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.owasp.validator.html.AntiSamy;
import org.owasp.validator.html.Policy;
@ -48,12 +49,18 @@ public class NoXssValidator implements ConstraintValidator<NoXss, Object> {
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
if (!(value instanceof String) || ((String) value).isEmpty()) {
String stringValue;
if (value instanceof CharSequence || value instanceof JsonNode) {
stringValue = value.toString();
} else {
return true;
}
if (stringValue.isEmpty()) {
return true;
}
try {
return xssChecker.scan((String) value, xssPolicy).getNumberOfErrors() == 0;
return xssChecker.scan(stringValue, xssPolicy).getNumberOfErrors() == 0;
} catch (ScanException | PolicyException e) {
return false;
}

19
dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java

@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
@ -66,6 +67,24 @@ public class UserDataValidator extends DataValidator<User> {
}
}
@Override
protected User validateUpdate(TenantId tenantId, User user) {
User old = userDao.findById(user.getTenantId(), user.getId().getId());
if (old == null) {
throw new DataValidationException("Can't update non existing user!");
}
if (!old.getTenantId().equals(user.getTenantId())) {
throw new DataValidationException("Can't update user tenant id!");
}
if (!old.getAuthority().equals(user.getAuthority())) {
throw new DataValidationException("Can't update user authority!");
}
if (!old.getCustomerId().equals(user.getCustomerId())) {
throw new DataValidationException("Can't update user customer id!");
}
return old;
}
@Override
protected void validateDataImpl(TenantId requestTenantId, User user) {
if (StringUtils.isEmpty(user.getEmail())) {

17
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java

@ -140,6 +140,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository {
selectPart.append(" a.originator_id as entity_id ");
}
EntityDataSortOrder sortOrder = pageLink.getSortOrder();
String textSearchQuery = buildTextSearchQuery(ctx, query.getAlarmFields(), pageLink.getTextSearch());
if (sortOrder != null && sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) {
String sortOrderKey = sortOrder.getKey().getKey();
sortPart.append(alarmFieldColumnMap.getOrDefault(sortOrderKey, sortOrderKey))
@ -166,7 +167,11 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository {
}
joinPart.append(" as e(id, priority)) e ");
if (pageLink.isSearchPropagatedAlarms()) {
joinPart.append("on ea.entity_id = e.id");
if (textSearchQuery.isEmpty()) {
joinPart.append("on ea.entity_id = e.id");
} else {
joinPart.append("on a.entity_id = e.id");
}
} else {
joinPart.append("on a.originator_id = e.id");
}
@ -230,13 +235,11 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository {
}
}
String textSearchQuery = buildTextSearchQuery(ctx, query.getAlarmFields(), pageLink.getTextSearch());
String mainQuery;
if (!textSearchQuery.isEmpty()) {
mainQuery = selectPart.toString() + fromPart.toString() + wherePart.toString();
mainQuery = String.format("select * from (%s) a %s WHERE %s", mainQuery, joinPart, textSearchQuery);
String mainQuery = String.format("%s%s", selectPart, fromPart);
if (textSearchQuery.isEmpty()) {
mainQuery = String.format("%s%s%s", mainQuery, joinPart, wherePart);
} else {
mainQuery = selectPart.toString() + fromPart.toString() + joinPart.toString() + wherePart.toString();
mainQuery = String.format("select * from (%s%s) a %s WHERE %s", mainQuery, wherePart, joinPart, textSearchQuery);
}
String countQuery = String.format("select count(*) from (%s) result", mainQuery);
long queryTs = System.currentTimeMillis();

5
dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java

@ -105,11 +105,6 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq
return Futures.immediateFuture(null);
}
@Override
public ListenableFuture<Void> removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
return Futures.immediateFuture(null);
}
@Override
public ListenableFuture<List<ReadTsKvQueryResult>> findAllAsync(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries) {
return processFindAllAsync(tenantId, entityId, queries);

8
dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java

@ -18,7 +18,6 @@ package org.thingsboard.server.dao.sqlts.timescale;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
@ -36,7 +35,6 @@ import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity;
import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity;
import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams;
import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper;
import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao;
@ -52,7 +50,6 @@ import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
@Component
@ -144,11 +141,6 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements
});
}
@Override
public ListenableFuture<Void> removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
return service.submit(() -> null);
}
@Override
public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
if (query.getAggregation() == Aggregation.NONE) {

2
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -46,7 +46,6 @@ import org.thingsboard.server.dao.service.Validator;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@ -277,7 +276,6 @@ public class BaseTimeseriesService implements TimeseriesService {
private void deleteAndRegisterFutures(TenantId tenantId, List<ListenableFuture<TsKvLatestRemovingResult>> futures, EntityId entityId, DeleteTsKvQuery query) {
futures.add(Futures.transform(timeseriesDao.remove(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor()));
futures.add(timeseriesLatestDao.removeLatest(tenantId, entityId, query));
futures.add(Futures.transform(timeseriesDao.removePartition(tenantId, entityId, query), v -> null, MoreExecutors.directExecutor()));
}
private static void validate(EntityId entityId) {

79
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -28,6 +28,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -59,7 +60,6 @@ import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@ -82,17 +82,21 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
protected static final int MIN_AGGREGATION_STEP_MS = 1000;
public static final String ASC_ORDER = "ASC";
public static final long SECONDS_IN_DAY = TimeUnit.DAYS.toSeconds(1);
protected static List<Long> FIXED_PARTITION = Arrays.asList(new Long[]{0L});
protected static final List<Long> FIXED_PARTITION = List.of(0L);
private CassandraTsPartitionsCache cassandraTsPartitionsCache;
@Autowired
private Environment environment;
@Getter
@Value("${cassandra.query.ts_key_value_partitioning}")
private String partitioning;
@Getter
@Value("${cassandra.query.use_ts_key_value_partitioning_on_read:true}")
private boolean useTsKeyValuePartitioningOnRead;
@Value("${cassandra.query.ts_key_value_partitions_max_cache_size:100000}")
private long partitionsCacheSize;
@ -222,46 +226,6 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
return resultFuture;
}
@Override
public ListenableFuture<Void> removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
long minPartition = toPartitionTs(query.getStartTs());
long maxPartition = toPartitionTs(query.getEndTs());
if (minPartition == maxPartition) {
return Futures.immediateFuture(null);
} else {
TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition);
final SimpleListenableFuture<Void> resultFuture = new SimpleListenableFuture<>();
final ListenableFuture<List<Long>> partitionsListFuture = Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
Futures.addCallback(partitionsListFuture, new FutureCallback<List<Long>>() {
@Override
public void onSuccess(@Nullable List<Long> partitions) {
int index = 0;
if (minPartition != query.getStartTs()) {
index = 1;
}
List<Long> partitionsToDelete = new ArrayList<>();
for (int i = index; i < partitions.size() - 1; i++) {
partitionsToDelete.add(partitions.get(i));
}
QueryCursor cursor = new QueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitionsToDelete);
deletePartitionAsync(tenantId, cursor, resultFuture);
for (Long partition : partitionsToDelete) {
cassandraTsPartitionsCache.invalidate(new CassandraPartitionCacheKey(entityId, query.getKey(), partition));
}
}
@Override
public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t);
}
}, readResultsProcessingExecutor);
return resultFuture;
}
}
@Override
public ListenableFuture<ReadTsKvQueryResult> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) {
if (query.getAggregation() == Aggregation.NONE) {
@ -337,7 +301,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
}, MoreExecutors.directExecutor());
}
private long toPartitionTs(long ts) {
long toPartitionTs(long ts) {
LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC);
return tsFormat.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli();
}
@ -417,10 +381,37 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
if (isFixedPartitioning()) { //no need to fetch partitions from DB
return Futures.immediateFuture(FIXED_PARTITION);
}
if (!isUseTsKeyValuePartitioningOnRead()) {
return Futures.immediateFuture(calculatePartitions(minPartition, maxPartition));
}
TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition);
return Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor);
}
List<Long> calculatePartitions(long minPartition, long maxPartition) {
if (minPartition == maxPartition) {
return Collections.singletonList(minPartition);
}
List<Long> partitions = new ArrayList<>();
long currentPartition = minPartition;
LocalDateTime currentPartitionTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(currentPartition), ZoneOffset.UTC);
while (maxPartition > currentPartition) {
partitions.add(currentPartition);
currentPartitionTime = calculateNextPartition(currentPartitionTime);
currentPartition = currentPartitionTime.toInstant(ZoneOffset.UTC).toEpochMilli();
}
partitions.add(maxPartition);
return partitions;
}
private LocalDateTime calculateNextPartition(LocalDateTime time) {
return time.plus(1, tsFormat.getTruncateUnit());
}
private AsyncFunction<List<Long>, List<TbResultSet>> getFetchChunksAsyncFunction(TenantId tenantId, EntityId entityId, String key, Aggregation aggregation, long startTs, long endTs) {
return partitions -> {
try {

3
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java

@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import java.util.List;
import java.util.Map;
/**
* @author Andrew Shvayka
@ -39,7 +38,5 @@ public interface TimeseriesDao {
ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);
ListenableFuture<Void> removePartition(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query);
void cleanup(long systemTtl);
}

39
dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java

@ -15,23 +15,16 @@
*/
package org.thingsboard.server.dao.service;
import org.junit.jupiter.api.BeforeAll;
import com.fasterxml.jackson.databind.node.TextNode;
import org.junit.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.asset.Asset;
import javax.validation.ConstraintValidatorContext;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.Mockito.mock;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class NoXssValidatorTest {
private static NoXssValidator validator;
@BeforeAll
public static void beforeAll() {
validator = new NoXssValidator();
validator.initialize(null);
}
@ParameterizedTest
@ValueSource(strings = {
@ -44,9 +37,25 @@ public class NoXssValidatorTest {
" <img src= \"http://site.com/\" > ",
"123 <input type=text value=a onfocus=alert(1337) AUTOFOCUS>bebe"
})
public void testIsNotValid(String stringWithXss) {
boolean isValid = validator.isValid(stringWithXss, mock(ConstraintValidatorContext.class));
assertFalse(isValid);
public void givenEntityWithMaliciousPropertyValue_thenReturnValidationError(String maliciousString) {
Asset invalidAsset = new Asset();
invalidAsset.setName(maliciousString);
assertThatThrownBy(() -> {
ConstraintValidator.validateFields(invalidAsset);
}).hasMessageContaining("field value is malformed");
}
@Test
public void givenEntityWithMaliciousValueInAdditionalInfo_thenReturnValidationError() {
Asset invalidAsset = new Asset();
String maliciousValue = "qwerty<script>alert(document.cookie)</script>qwerty";
invalidAsset.setAdditionalInfo(JacksonUtil.newObjectNode()
.set("description", new TextNode(maliciousValue)));
assertThatThrownBy(() -> {
ConstraintValidator.validateFields(invalidAsset);
}).hasMessageContaining("field value is malformed");
}
}

133
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningDaysAlwaysExistsTest.java

@ -0,0 +1,133 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=DAYS",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningDaysAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsDays() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("DAYS");
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:00:01Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T00:00:00Z").getTime());
}
@Test
public void testCalculatePartitionsDays() throws ParseException {
long startTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime());
long nextTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-12T23:59:59Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-15T00:00:00Z").getTime());
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs);
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L));
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L));
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-12T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(6).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-12T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-13T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-14T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-15T00:00:00Z").getTime()));
long leapStartTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-27T00:00:00Z").getTime());
long leapEndTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-03-01T00:00:00Z").getTime());
assertThat(tsDao.calculatePartitions(leapStartTs, leapEndTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-27T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-28T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-29T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-03-01T00:00:00Z").getTime()));
long newYearStartTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-30T00:00:00Z").getTime());
long newYearEndTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime());
assertThat(tsDao.calculatePartitions(newYearStartTs, newYearEndTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-30T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-31T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime()));
}
}

134
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningHoursAlwaysExistsTest.java

@ -0,0 +1,134 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=HOURS",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningHoursAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsHours() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("HOURS");
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T01:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T01:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T02:00:01Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T02:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:00:00Z").getTime());
}
@Test
public void testCalculatePartitionsHours() throws ParseException {
long startTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime());
long nextTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T03:59:59Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:59:00Z").getTime());
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs);
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L));
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L));
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T01:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T02:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T03:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(25).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T01:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T02:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T03:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T04:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T05:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T06:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T07:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T08:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T09:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T10:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T11:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T12:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T13:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T14:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T15:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T16:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T17:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T18:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T19:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T20:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T21:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T22:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T23:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:00:00Z").getTime()));
}
}

78
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningIndefiniteAlwaysExistsTest.java

@ -0,0 +1,78 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=INDEFINITE",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningIndefiniteAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsIndefinite() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("INDEFINITE");
assertThat(tsDao.toPartitionTs(ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(0L);
}
@Test
public void testCalculatePartitionsIndefinite() throws ParseException {
//Indefinite partitioning should never call tsDao.calculatePartitions()
}
}

121
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java

@ -0,0 +1,121 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=MINUTES",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsMinutes() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("MINUTES");
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:01:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:01:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:02:01Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:02:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:00Z").getTime());
}
@Test
public void testCalculatePartitionsMinutes() throws ParseException {
long startTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime());
long nextTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:02:59Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:10:00Z").getTime());
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs);
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L));
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L));
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:01:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:02:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(11).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:01:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:02:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:03:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:04:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:05:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:06:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:07:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:08:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:09:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:10:00Z").getTime()));
}
}

134
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java

@ -0,0 +1,134 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=MONTHS",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsMonths() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("MONTHS");
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(1640995200000L).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime())).isEqualTo(1651363200000L).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:01Z").getTime())).isEqualTo(1651363200000L).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(1651363200000L).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(1701388800000L).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-01T00:00:00Z").getTime());
}
@Test
public void testCalculatePartitionsMonths() throws ParseException {
long startTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime());
long nextTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-31T23:59:59Z").getTime());
long leapTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-29T23:59:59Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime());
log.info("startTs {}, nextTs {}, leapTs {}, endTs {}", startTs, nextTs, leapTs, endTs);
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L));
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L));
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(1575158400000L)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(1575158400000L, 1577836800000L)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, leapTs)).isEqualTo(List.of(1575158400000L, 1577836800000L, 1580515200000L)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-01T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(14).isEqualTo(List.of(
1575158400000L,
1577836800000L, 1580515200000L, 1583020800000L,
1585699200000L, 1588291200000L, 1590969600000L,
1593561600000L, 1596240000000L, 1598918400000L,
1601510400000L, 1604188800000L, 1606780800000L,
1609459200000L)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-03-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-04-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-05-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-06-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-07-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-08-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-09-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-10-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-11-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime()));
}
}

116
dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java

@ -0,0 +1,116 @@
/**
* Copyright © 2016-2022 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.timeseries;
import lombok.extern.slf4j.Slf4j;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
import java.text.ParseException;
import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class)
@TestPropertySource(properties = {
"database.ts.type=cassandra",
"cassandra.query.ts_key_value_partitioning=YEARS",
"cassandra.query.use_ts_key_value_partitioning_on_read=false",
"cassandra.query.ts_key_value_partitions_max_cache_size=100000",
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true",
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60",
"cassandra.query.ts_key_value_ttl=0",
"cassandra.query.set_null_values_enabled=false",
})
@Slf4j
public class CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest {
@Autowired
CassandraBaseTimeseriesDao tsDao;
@MockBean(answer = Answers.RETURNS_MOCKS)
@Qualifier("CassandraCluster")
CassandraCluster cassandraCluster;
@MockBean
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
@MockBean
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
@Test
public void testToPartitionsYears() throws ParseException {
assertThat(tsDao.getPartitioning()).isEqualTo("YEARS");
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:01Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime());
assertThat(tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-01-01T00:00:00Z").getTime());
}
@Test
public void testCalculatePartitionsYears() throws ParseException {
long startTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime());
long nextTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-10-12T23:59:59Z").getTime());
long endTs = tsDao.toPartitionTs(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2025-07-15T00:00:00Z").getTime());
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs);
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L));
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L));
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime()));
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(7).isEqualTo(List.of(
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2024-01-01T00:00:00Z").getTime(),
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2025-01-01T00:00:00Z").getTime()));
}
}

3
docker/tb-js-executor.env

@ -3,4 +3,5 @@ LOGGER_LEVEL=info
LOG_FOLDER=logs
LOGGER_FILENAME=tb-js-executor-%DATE%.log
DOCKER_MODE=true
SCRIPT_BODY_TRACE_FREQUENCY=1000
SCRIPT_BODY_TRACE_FREQUENCY=1000
NODE_OPTIONS="--max-old-space-size=200"

1
lombok.config

@ -1,2 +1,3 @@
config.stopbubbling = true
lombok.anyconstructor.addconstructorproperties = true
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy

4
msa/black-box-tests/README.md

@ -30,5 +30,9 @@ As result, in REPOSITORY column, next images should be present:
mvn clean install -DblackBoxTests.skip=false -DblackBoxTests.hybridMode=true
To run the black box tests with using local env run tests in the [msa/black-box-tests](../black-box-tests) directory with runLocal property:
mvn clean install -DblackBoxTests.skip=false -DrunLocal=true

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

@ -57,11 +57,6 @@
<artifactId>httpclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.takari.junit</groupId>
<artifactId>takari-cpsuite</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@ -72,6 +67,26 @@
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
@ -152,11 +167,18 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*TestSuite.java</include>
</includes>
<suiteXmlFiles>
<suiteXmlFile>src/test/resources/testNG.xml</suiteXmlFile>
</suiteXmlFiles>
<skipTests>${blackBoxTests.skip}</skipTests>
</configuration>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-testng</artifactId>
<version>${surefire.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

130
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java

@ -15,115 +15,59 @@
*/
package org.thingsboard.server.msa;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.thingsboard.rest.client.RestClient;
import org.thingsboard.server.common.data.Device;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Listeners;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
import javax.net.ssl.SSLContext;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Random;
@Slf4j
@Listeners(TestListener.class)
public abstract class AbstractContainerTest {
protected static final String HTTPS_URL = "https://localhost";
protected static final String WSS_URL = "wss://localhost";
protected static String TB_TOKEN;
protected static RestClient restClient;
protected static long timeoutMultiplier = 1;
protected ObjectMapper mapper = new ObjectMapper();
protected JsonParser jsonParser = new JsonParser();
@BeforeClass
public static void before() throws Exception {
restClient = new RestClient(HTTPS_URL);
restClient.getRestTemplate().setRequestFactory(getRequestFactoryForSelfSignedCert());
private static final ContainerTestSuite containerTestSuite = ContainerTestSuite.getInstance();
protected static TestRestClient testRestClient;
@BeforeSuite
public void beforeSuite() {
if ("false".equals(System.getProperty("runLocal", "false"))) {
containerTestSuite.start();
}
testRestClient = new TestRestClient(TestProperties.getBaseUrl());
if (!"kafka".equals(System.getProperty("blackBoxTests.queue", "kafka"))) {
timeoutMultiplier = 10;
}
}
@Rule
public TestRule watcher = new TestWatcher() {
protected void starting(Description description) {
log.info("=================================================");
log.info("STARTING TEST: {}" , description.getMethodName());
log.info("=================================================");
@AfterSuite
public void afterSuite() {
if (containerTestSuite.isActive()) {
containerTestSuite.stop();
}
/**
* Invoked when a test succeeds
*/
protected void succeeded(Description description) {
log.info("=================================================");
log.info("SUCCEEDED TEST: {}" , description.getMethodName());
log.info("=================================================");
}
/**
* Invoked when a test fails
*/
protected void failed(Throwable e, Description description) {
log.info("=================================================");
log.info("FAILED TEST: {}" , description.getMethodName(), e);
log.info("=================================================");
}
};
protected Device createGatewayDevice() throws JsonProcessingException {
String isGateway = "{\"gateway\":true}";
ObjectMapper objectMapper = new ObjectMapper();
JsonNode additionalInfo = objectMapper.readTree(isGateway);
Device gatewayDeviceTemplate = new Device();
gatewayDeviceTemplate.setName("mqtt_gateway");
gatewayDeviceTemplate.setType("gateway");
gatewayDeviceTemplate.setAdditionalInfo(additionalInfo);
return restClient.saveDevice(gatewayDeviceTemplate);
}
protected Device createDevice(String name) {
Device device = new Device();
device.setName(name + StringUtils.randomAlphanumeric(7));
device.setType("DEFAULT");
return restClient.saveDevice(device);
}
protected WsClient subscribeToWebSocket(DeviceId deviceId, String scope, CmdsType property) throws Exception {
WsClient wsClient = new WsClient(new URI(WSS_URL + "/api/ws/plugins/telemetry?token=" + restClient.getToken()), timeoutMultiplier);
SSLContextBuilder builder = SSLContexts.custom();
builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true);
wsClient.setSocketFactory(builder.build().getSocketFactory());
String webSocketUrl = TestProperties.getWebSocketUrl();
WsClient wsClient = new WsClient(new URI(webSocketUrl + "/api/ws/plugins/telemetry?token=" + testRestClient.getToken()), timeoutMultiplier);
if (webSocketUrl.matches("^(wss)://.*$")) {
SSLContextBuilder builder = SSLContexts.custom();
builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true);
wsClient.setSocketFactory(builder.build().getSocketFactory());
}
wsClient.connectBlocking();
JsonObject cmdsObject = new JsonObject();
@ -150,16 +94,6 @@ public abstract class AbstractContainerTest {
.build();
}
protected boolean verify(WsTelemetryResponse wsTelemetryResponse, String key, Long expectedTs, String expectedValue) {
List<Object> list = wsTelemetryResponse.getDataValuesByKey(key);
return expectedTs.equals(list.get(0)) && expectedValue.equals(list.get(1));
}
protected boolean verify(WsTelemetryResponse wsTelemetryResponse, String key, String expectedValue) {
List<Object> list = wsTelemetryResponse.getDataValuesByKey(key);
return expectedValue.equals(list.get(1));
}
protected JsonObject createGatewayConnectPayload(String deviceName){
JsonObject payload = new JsonObject();
payload.addProperty("device", deviceName);
@ -216,20 +150,4 @@ public abstract class AbstractContainerTest {
}
}
private static HttpComponentsClientHttpRequestFactory getRequestFactoryForSelfSignedCert() throws Exception {
SSLContextBuilder builder = SSLContexts.custom();
builder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true);
SSLContext sslContext = builder.build();
SSLConnectionSocketFactory sslSelfSigned = new SSLConnectionSocketFactory(sslContext, (s, sslSession) -> true);
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
.<ConnectionSocketFactory>create()
.register("https", sslSelfSigned)
.build();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
return new HttpComponentsClientHttpRequestFactory(httpClient);
}
}

221
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ContainerTestSuite.java

@ -17,9 +17,6 @@ package org.thingsboard.server.msa;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.junit.ClassRule;
import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.thingsboard.server.common.data.StringUtils;
@ -41,10 +38,8 @@ import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.fail;
import static org.testng.Assert.fail;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.msa.*Test"})
@Slf4j
public class ContainerTestSuite {
final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster"));
@ -57,107 +52,133 @@ public class ContainerTestSuite {
private static final String TB_JS_EXECUTOR_LOG_REGEXP = ".*template started.*";
private static final Duration CONTAINER_STARTUP_TIMEOUT = Duration.ofSeconds(400);
private static DockerComposeContainer<?> testContainer;
@ClassRule
public static ThingsBoardDbInstaller installTb = new ThingsBoardDbInstaller();
@ClassRule
public static DockerComposeContainer getTestContainer() {
if (testContainer == null) {
log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER);
log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE);
boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers"));
try {
final String targetDir = FileUtils.getTempDirectoryPath() + "/" + "ContainerTestSuite-" + UUID.randomUUID() + "/";
log.info("targetDir {}", targetDir);
FileUtils.copyDirectory(new File(SOURCE_DIR), new File(targetDir));
replaceInFile(targetDir + "docker-compose.yml", " container_name: \"${LOAD_BALANCER_NAME}\"", "", "container_name");
FileUtils.copyDirectory(new File("src/test/resources"), new File(targetDir));
class DockerComposeContainerImpl<SELF extends DockerComposeContainer<SELF>> extends DockerComposeContainer<SELF> {
public DockerComposeContainerImpl(List<File> composeFiles) {
super(composeFiles);
}
@Override
public void stop() {
super.stop();
tryDeleteDir(targetDir);
}
}
private DockerComposeContainer<?> testContainer;
private ThingsBoardDbInstaller installTb;
private boolean isActive;
private static ContainerTestSuite containerTestSuite;
public boolean isActive() {
return isActive;
}
public void setActive(boolean active) {
isActive = active;
}
private ContainerTestSuite() {
}
public static ContainerTestSuite getInstance() {
if (containerTestSuite == null) {
containerTestSuite = new ContainerTestSuite();
}
return containerTestSuite;
}
public void start() {
installTb = new ThingsBoardDbInstaller();
installTb.createVolumes();
log.info("System property of blackBoxTests.redisCluster is {}", IS_REDIS_CLUSTER);
log.info("System property of blackBoxTests.hybridMode is {}", IS_HYBRID_MODE);
boolean skipTailChildContainers = Boolean.valueOf(System.getProperty("blackBoxTests.skipTailChildContainers"));
try {
final String targetDir = FileUtils.getTempDirectoryPath() + "/" + "ContainerTestSuite-" + UUID.randomUUID() + "/";
log.info("targetDir {}", targetDir);
FileUtils.copyDirectory(new File(SOURCE_DIR), new File(targetDir));
replaceInFile(targetDir + "docker-compose.yml", " container_name: \"${LOAD_BALANCER_NAME}\"", "", "container_name");
List<File> composeFiles = new ArrayList<>(Arrays.asList(
new File(targetDir + "docker-compose.yml"),
new File(targetDir + "docker-compose.volumes.yml"),
new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")),
new File(targetDir + "docker-compose.postgres.volumes.yml"),
new File(targetDir + "docker-compose." + QUEUE_TYPE + ".yml"),
new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.yml" : "docker-compose.redis.yml")),
new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.volumes.yml" : "docker-compose.redis.volumes.yml"))
));
Map<String, String> queueEnv = new HashMap<>();
queueEnv.put("TB_QUEUE_TYPE", QUEUE_TYPE);
switch (QUEUE_TYPE) {
case "kafka":
composeFiles.add(new File(targetDir + "docker-compose.kafka.yml"));
break;
case "aws-sqs":
replaceInFile(targetDir, "queue-aws-sqs.env",
Map.of("YOUR_KEY", getSysProp("blackBoxTests.awsKey"),
"YOUR_SECRET", getSysProp("blackBoxTests.awsSecret"),
"YOUR_REGION", getSysProp("blackBoxTests.awsRegion")));
break;
case "rabbitmq":
composeFiles.add(new File(targetDir + "docker-compose.rabbitmq-server.yml"));
replaceInFile(targetDir, "queue-rabbitmq.env",
Map.of("localhost", "rabbitmq"));
break;
case "service-bus":
replaceInFile(targetDir, "queue-service-bus.env",
Map.of("YOUR_NAMESPACE_NAME", getSysProp("blackBoxTests.serviceBusNamespace"),
"YOUR_SAS_KEY_NAME", getSysProp("blackBoxTests.serviceBusSASPolicy")));
replaceInFile(targetDir, "queue-service-bus.env",
Map.of("YOUR_SAS_KEY", getSysProp("blackBoxTests.serviceBusPrimaryKey")));
break;
case "pubsub":
replaceInFile(targetDir, "queue-pubsub.env",
Map.of("YOUR_PROJECT_ID", getSysProp("blackBoxTests.pubSubProjectId"),
"YOUR_SERVICE_ACCOUNT", getSysProp("blackBoxTests.pubSubServiceAccount")));
break;
default:
throw new RuntimeException("Unsupported queue type: " + QUEUE_TYPE);
FileUtils.copyDirectory(new File("src/test/resources"), new File(targetDir));
class DockerComposeContainerImpl<SELF extends DockerComposeContainer<SELF>> extends DockerComposeContainer<SELF> {
public DockerComposeContainerImpl(List<File> composeFiles) {
super(composeFiles);
}
if (IS_HYBRID_MODE) {
composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml"));
@Override
public void stop() {
super.stop();
tryDeleteDir(targetDir);
}
}
testContainer = new DockerComposeContainerImpl<>(composeFiles)
.withPull(false)
.withLocalCompose(true)
.withTailChildContainers(!skipTailChildContainers)
.withEnv(installTb.getEnv())
.withEnv(queueEnv)
.withEnv("LOAD_BALANCER_NAME", "")
.withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-core1", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-core2", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-http-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-http-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-mqtt-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-mqtt-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT));
} catch (Exception e) {
log.error("Failed to create test container", e);
fail("Failed to create test container");
List<File> composeFiles = new ArrayList<>(Arrays.asList(
new File(targetDir + "docker-compose.yml"),
new File(targetDir + "docker-compose.volumes.yml"),
new File(targetDir + (IS_HYBRID_MODE ? "docker-compose.hybrid.yml" : "docker-compose.postgres.yml")),
new File(targetDir + "docker-compose.postgres.volumes.yml"),
new File(targetDir + "docker-compose." + QUEUE_TYPE + ".yml"),
new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.yml" : "docker-compose.redis.yml")),
new File(targetDir + (IS_REDIS_CLUSTER ? "docker-compose.redis-cluster.volumes.yml" : "docker-compose.redis.volumes.yml"))
));
Map<String, String> queueEnv = new HashMap<>();
queueEnv.put("TB_QUEUE_TYPE", QUEUE_TYPE);
switch (QUEUE_TYPE) {
case "kafka":
composeFiles.add(new File(targetDir + "docker-compose.kafka.yml"));
break;
case "aws-sqs":
replaceInFile(targetDir, "queue-aws-sqs.env",
Map.of("YOUR_KEY", getSysProp("blackBoxTests.awsKey"),
"YOUR_SECRET", getSysProp("blackBoxTests.awsSecret"),
"YOUR_REGION", getSysProp("blackBoxTests.awsRegion")));
break;
case "rabbitmq":
composeFiles.add(new File(targetDir + "docker-compose.rabbitmq-server.yml"));
replaceInFile(targetDir, "queue-rabbitmq.env",
Map.of("localhost", "rabbitmq"));
break;
case "service-bus":
replaceInFile(targetDir, "queue-service-bus.env",
Map.of("YOUR_NAMESPACE_NAME", getSysProp("blackBoxTests.serviceBusNamespace"),
"YOUR_SAS_KEY_NAME", getSysProp("blackBoxTests.serviceBusSASPolicy")));
replaceInFile(targetDir, "queue-service-bus.env",
Map.of("YOUR_SAS_KEY", getSysProp("blackBoxTests.serviceBusPrimaryKey")));
break;
case "pubsub":
replaceInFile(targetDir, "queue-pubsub.env",
Map.of("YOUR_PROJECT_ID", getSysProp("blackBoxTests.pubSubProjectId"),
"YOUR_SERVICE_ACCOUNT", getSysProp("blackBoxTests.pubSubServiceAccount")));
break;
default:
throw new RuntimeException("Unsupported queue type: " + QUEUE_TYPE);
}
if (IS_HYBRID_MODE) {
composeFiles.add(new File(targetDir + "docker-compose.cassandra.volumes.yml"));
}
testContainer = new DockerComposeContainerImpl<>(composeFiles)
.withPull(false)
.withLocalCompose(true)
.withTailChildContainers(!skipTailChildContainers)
.withEnv(installTb.getEnv())
.withEnv(queueEnv)
.withEnv("LOAD_BALANCER_NAME", "")
.withExposedService("haproxy", 80, Wait.forHttp("/swagger-ui.html").withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-core1", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-core2", Wait.forLogMessage(TB_CORE_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-http-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-http-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-mqtt-transport1", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-mqtt-transport2", Wait.forLogMessage(TRANSPORTS_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-vc-executor1", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-vc-executor2", Wait.forLogMessage(TB_VC_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT))
.waitingFor("tb-js-executor", Wait.forLogMessage(TB_JS_EXECUTOR_LOG_REGEXP, 1).withStartupTimeout(CONTAINER_STARTUP_TIMEOUT));
testContainer.start();
setActive(true);
} catch (Exception e) {
log.error("Failed to create test container", e);
fail("Failed to create test container");
}
}
public void stop() {
if (isActive) {
testContainer.stop();
installTb.savaLogsAndRemoveVolumes();
setActive(false);
}
return testContainer;
}
private static void replaceInFile(String targetDir, String fileName, Map<String, String> replacements) throws IOException {

53
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2022 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 org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.TestListenerAdapter;
import static org.testng.internal.Utils.log;
@Slf4j
public class TestListener extends TestListenerAdapter {
@Override
public void onTestStart(ITestResult result) {
super.onTestStart(result);
log.info("===>>> Test started: " + result.getName());
}
/**
* Invoked when a test succeeds
*/
@Override
public void onTestSuccess(ITestResult result) {
super.onTestSuccess(result);
if (result != null) {
log.info("<<<=== Test completed successfully: " + result.getName());
}
}
/**
* Invoked when a test fails
*/
@Override
public void onTestFailure(ITestResult result) {
super.onTestFailure(result);
log.info("<<<=== Test failed: " + result.getName());
}
}

61
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestProperties.java

@ -0,0 +1,61 @@
/**
* Copyright © 2016-2022 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.io.InputStream;
import java.util.Properties;
@Slf4j
public class TestProperties {
private static final String HTTPS_URL = "https://localhost";
private static final String WSS_URL = "wss://localhost";
private static final ContainerTestSuite instance = ContainerTestSuite.getInstance();
private static Properties properties;
public static String getBaseUrl() {
if (instance.isActive()) {
return HTTPS_URL;
}
return getProperties().getProperty("tb.baseUrl");
}
public static String getWebSocketUrl() {
if (instance.isActive()) {
return WSS_URL;
}
return getProperties().getProperty("tb.wsUrl");
}
private static Properties getProperties() {
if (properties == null) {
try (InputStream input = TestProperties.class.getClassLoader().getResourceAsStream("config.properties")) {
properties = new Properties();
properties.load(input);
} catch (IOException ex) {
log.error("Exception while reading test properties " + ex.getMessage());
}
}
return properties;
}
}

262
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java

@ -0,0 +1,262 @@
/**
* Copyright © 2016-2022 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 com.fasterxml.jackson.databind.JsonNode;
import io.restassured.RestAssured;
import io.restassured.common.mapper.TypeRef;
import io.restassured.config.HeaderConfig;
import io.restassured.config.RestAssuredConfig;
import io.restassured.filter.log.RequestLoggingFilter;
import io.restassured.filter.log.ResponseLoggingFilter;
import io.restassured.http.ContentType;
import io.restassured.path.json.JsonPath;
import io.restassured.response.ValidatableResponse;
import io.restassured.specification.RequestSpecification;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static io.restassured.RestAssured.given;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import static java.net.HttpURLConnection.HTTP_OK;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.core.AnyOf.anyOf;
import static org.thingsboard.server.common.data.StringUtils.isEmpty;
public class TestRestClient {
private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
private static final String CONTENT_TYPE_HEADER = "Content-Type";
private final RequestSpecification requestSpec;
private String token;
private String refreshToken;
public TestRestClient(String url) {
RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter());
requestSpec = given().baseUri(url)
.contentType(ContentType.JSON)
.config(RestAssuredConfig.config()
.headerConfig(HeaderConfig.headerConfig()
.overwriteHeadersWithName(JWT_TOKEN_HEADER_PARAM, CONTENT_TYPE_HEADER)));
if (url.matches("^(https)://.*$")) {
requestSpec.relaxedHTTPSValidation();
}
}
public void login(String username, String password) {
Map<String, String> loginRequest = new HashMap<>();
loginRequest.put("username", username);
loginRequest.put("password", password);
JsonPath jsonPath = given().spec(requestSpec).body(loginRequest)
.post( "/api/auth/login")
.getBody().jsonPath();
token = jsonPath.get("token");
refreshToken = jsonPath.get("refreshToken");
requestSpec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
}
public Device postDevice(String accessToken, Device device) {
return given().spec(requestSpec).body(device)
.pathParams("accessToken", accessToken)
.post("/api/device?accessToken={accessToken}")
.then()
.statusCode(HTTP_OK)
.extract()
.as(Device.class);
}
public ValidatableResponse getDeviceById(DeviceId deviceId, int statusCode) {
return given().spec(requestSpec)
.pathParams("deviceId", deviceId.getId())
.get("/api/device/{deviceId}")
.then()
.statusCode(statusCode);
}
public Device getDeviceById(DeviceId deviceId) {
return getDeviceById(deviceId, HTTP_OK)
.extract()
.as(Device.class);
}
public DeviceCredentials getDeviceCredentialsByDeviceId(DeviceId deviceId) {
return given().spec(requestSpec).get("/api/device/{deviceId}/credentials", deviceId.getId())
.then()
.assertThat()
.statusCode(HTTP_OK)
.extract()
.as(DeviceCredentials.class);
}
public ValidatableResponse postTelemetry(String credentialsId, JsonNode telemetry) {
return given().spec(requestSpec).body(telemetry)
.post("/api/v1/{credentialsId}/telemetry", credentialsId)
.then()
.statusCode(HTTP_OK);
}
public ValidatableResponse deleteDevice(DeviceId deviceId) {
return given().spec(requestSpec)
.delete("/api/device/{deviceId}", deviceId.getId())
.then()
.statusCode(HTTP_OK);
}
public ValidatableResponse deleteDeviceIfExists(DeviceId deviceId) {
return given().spec(requestSpec)
.delete("/api/device/{deviceId}", deviceId.getId())
.then()
.statusCode(anyOf(is(HTTP_OK),is(HTTP_NOT_FOUND)));
}
public ValidatableResponse postTelemetryAttribute(String entityType, DeviceId deviceId, String scope, JsonNode attribute) {
return given().spec(requestSpec).body(attribute)
.post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityType, deviceId.getId(), scope)
.then()
.statusCode(HTTP_OK);
}
public ValidatableResponse postAttribute(String accessToken, JsonNode attribute) {
return given().spec(requestSpec).body(attribute)
.post("/api/v1/{accessToken}/attributes/", accessToken)
.then()
.statusCode(HTTP_OK);
}
public JsonNode getAttributes(String accessToken, String clientKeys, String sharedKeys) {
return given().spec(requestSpec)
.queryParam("clientKeys", clientKeys)
.queryParam("sharedKeys", sharedKeys)
.get("/api/v1/{accessToken}/attributes", accessToken)
.then()
.statusCode(HTTP_OK)
.extract()
.as(JsonNode.class);
}
public PageData<RuleChain> getRuleChains(PageLink pageLink) {
Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink);
return given().spec(requestSpec).queryParams(params)
.get("/api/ruleChains")
.then()
.statusCode(HTTP_OK)
.extract()
.as(new TypeRef<PageData<RuleChain>>() {});
}
public RuleChain postRootRuleChain(RuleChain ruleChain) {
return given().spec(requestSpec)
.body(ruleChain)
.post("/api/ruleChain")
.then()
.statusCode(HTTP_OK)
.extract()
.as(RuleChain.class);
}
public RuleChainMetaData postRuleChainMetadata(RuleChainMetaData ruleChainMetaData) {
return given().spec(requestSpec)
.body(ruleChainMetaData)
.post("/api/ruleChain/metadata")
.then()
.statusCode(HTTP_OK)
.extract()
.as(RuleChainMetaData.class);
}
public void setRootRuleChain(RuleChainId ruleChainId) {
given().spec(requestSpec)
.post("/api/ruleChain/{ruleChainId}/root", ruleChainId.getId())
.then()
.statusCode(HTTP_OK);
}
public void deleteRuleChain(RuleChainId ruleChainId) {
given().spec(requestSpec)
.delete("/api/ruleChain/{ruleChainId}", ruleChainId.getId())
.then()
.statusCode(HTTP_OK);
}
private String getUrlParams(PageLink pageLink) {
String urlParams = "pageSize={pageSize}&page={page}";
if (!isEmpty(pageLink.getTextSearch())) {
urlParams += "&textSearch={textSearch}";
}
if (pageLink.getSortOrder() != null) {
urlParams += "&sortProperty={sortProperty}&sortOrder={sortOrder}";
}
return urlParams;
}
private void addPageLinkToParam(Map<String, String> params, PageLink pageLink) {
params.put("pageSize", String.valueOf(pageLink.getPageSize()));
params.put("page", String.valueOf(pageLink.getPage()));
if (!isEmpty(pageLink.getTextSearch())) {
params.put("textSearch", pageLink.getTextSearch());
}
if (pageLink.getSortOrder() != null) {
params.put("sortProperty", pageLink.getSortOrder().getProperty());
params.put("sortOrder", pageLink.getSortOrder().getDirection().name());
}
}
public List<EntityRelation> findRelationByFrom(EntityId fromId, RelationTypeGroup relationTypeGroup) {
Map<String, String> params = new HashMap<>();
params.put("fromId", fromId.getId().toString());
params.put("fromType", fromId.getEntityType().name());
params.put("relationTypeGroup", relationTypeGroup.name());
return given().spec(requestSpec)
.pathParams(params)
.get("/api/relations?fromId={fromId}&fromType={fromType}&relationTypeGroup={relationTypeGroup}")
.then()
.statusCode(HTTP_OK)
.extract()
.as(new TypeRef<List<EntityRelation>>() {});
}
public JsonNode postServerSideRpc(DeviceId deviceId, JsonNode serverRpcPayload) {
return given().spec(requestSpec)
.body(serverRpcPayload)
.post("/api/rpc/twoway/{deviceId}", deviceId.getId())
.then()
.statusCode(HTTP_OK)
.extract()
.as(JsonNode.class);
}
public String getToken() {
return token;
}
public String getRefreshToken() {
return refreshToken;
}
}

9
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java

@ -16,7 +16,6 @@
package org.thingsboard.server.msa;
import lombok.extern.slf4j.Slf4j;
import org.junit.rules.ExternalResource;
import org.testcontainers.utility.Base58;
import org.thingsboard.server.common.data.StringUtils;
@ -30,7 +29,7 @@ import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Slf4j
public class ThingsBoardDbInstaller extends ExternalResource {
public class ThingsBoardDbInstaller {
final static boolean IS_REDIS_CLUSTER = Boolean.parseBoolean(System.getProperty("blackBoxTests.redisCluster"));
final static boolean IS_HYBRID_MODE = Boolean.parseBoolean(System.getProperty("blackBoxTests.hybridMode"));
@ -129,8 +128,7 @@ public class ThingsBoardDbInstaller extends ExternalResource {
return env;
}
@Override
protected void before() throws Throwable {
public void createVolumes() {
try {
dockerCompose.withCommand("volume create " + postgresDataVolume);
@ -192,8 +190,7 @@ public class ThingsBoardDbInstaller extends ExternalResource {
}
}
@Override
protected void after() {
public void savaLogsAndRemoveVolumes() {
copyLogs(tbLogVolume, "./target/tb-logs/");
copyLogs(tbCoapTransportLogVolume, "./target/tb-coap-transport-logs/");
copyLogs(tbLwm2mTransportLogVolume, "./target/tb-lwm2m-transport-logs/");

112
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java

@ -16,109 +16,79 @@
package org.thingsboard.server.msa.connectivity;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Sets;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.http.ResponseEntity;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.msa.AbstractContainerTest;
import org.thingsboard.server.msa.WsClient;
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
import java.util.Optional;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.common.data.DataConstants.DEVICE;
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
public class HttpClientTest extends AbstractContainerTest {
private Device device;
@BeforeMethod
public void setUp() throws Exception {
testRestClient.login("tenant@thingsboard.org", "tenant");
device = testRestClient.postDevice("", defaultDevicePrototype("http_"));
}
@AfterMethod
public void tearDown() {
testRestClient.deleteDeviceIfExists(device.getId());
}
@Test
public void telemetryUpload() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
Device device = createDevice("http_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
ResponseEntity deviceTelemetryResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/v1/{credentialsId}/telemetry",
mapper.readTree(createPayload().toString()),
ResponseEntity.class,
deviceCredentials.getCredentialsId());
Assert.assertTrue(deviceTelemetryResponse.getStatusCode().is2xxSuccessful());
testRestClient.postTelemetry(deviceCredentials.getCredentialsId(), mapper.readTree(createPayload().toString()));
WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
wsClient.closeBlocking();
Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"),
actualLatestTelemetry.getLatestValues().keySet());
Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString()));
Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1"));
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0)));
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73)));
assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey"));
restClient.deleteDevice(device.getId());
assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
}
@Test
public void getAttributes() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
TB_TOKEN = restClient.getToken();
String accessToken = testRestClient.getDeviceCredentialsByDeviceId(device.getId()).getCredentialsId();
assertThat(accessToken).isNotNull();
Device device = createDevice("test");
String accessToken = restClient.getDeviceCredentialsByDeviceId(device.getId()).get().getCredentialsId();
assertNotNull(accessToken);
JsonNode sharedAattribute = mapper.readTree(createPayload().toString());
testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, sharedAattribute);
ResponseEntity deviceSharedAttributes = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/" + DEVICE + "/" + device.getId().toString() + "/attributes/" + SHARED_SCOPE, mapper.readTree(createPayload().toString()),
ResponseEntity.class,
accessToken);
Assert.assertTrue(deviceSharedAttributes.getStatusCode().is2xxSuccessful());
ResponseEntity deviceClientsAttributes = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/v1/" + accessToken + "/attributes/", mapper.readTree(createPayload().toString()),
ResponseEntity.class,
accessToken);
Assert.assertTrue(deviceClientsAttributes.getStatusCode().is2xxSuccessful());
JsonNode clientAttribute = mapper.readTree(createPayload().toString());
testRestClient.postAttribute(accessToken, clientAttribute);
TimeUnit.SECONDS.sleep(3 * timeoutMultiplier);
@SuppressWarnings("deprecation")
Optional<JsonNode> allOptional = restClient.getAttributes(accessToken, null, null);
assertTrue(allOptional.isPresent());
JsonNode all = allOptional.get();
assertEquals(2, all.size());
assertEquals(mapper.readTree(createPayload().toString()), all.get("shared"));
assertEquals(mapper.readTree(createPayload().toString()), all.get("client"));
@SuppressWarnings("deprecation")
Optional<JsonNode> sharedOptional = restClient.getAttributes(accessToken, null, "stringKey");
assertTrue(sharedOptional.isPresent());
JsonNode shared = sharedOptional.get();
assertEquals(shared.get("shared").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString()));
assertFalse(shared.has("client"));
JsonNode attributes = testRestClient.getAttributes(accessToken, null, null);
assertThat(attributes.get("shared")).isEqualTo(sharedAattribute);
assertThat(attributes.get("client")).isEqualTo(clientAttribute);
@SuppressWarnings("deprecation")
Optional<JsonNode> clientOptional = restClient.getAttributes(accessToken, "longKey,stringKey", null);
assertTrue(clientOptional.isPresent());
JsonNode attributes2 = testRestClient.getAttributes(accessToken, null, "stringKey");
assertThat(attributes2.get("shared").get("stringKey")).isEqualTo(sharedAattribute.get("stringKey"));
assertThat(attributes2.has("client")).isFalse();
JsonNode client = clientOptional.get();
assertFalse(client.has("shared"));
assertEquals(mapper.readTree(createPayload().get("longKey").toString()), client.get("client").get("longKey"));
assertEquals(client.get("client").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString()));
JsonNode attributes3 = testRestClient.getAttributes(accessToken, "longKey,stringKey", null);
restClient.deleteDevice(device.getId());
assertThat(attributes3.has("shared")).isFalse();
assertThat(attributes3.get("client").get("longKey")).isEqualTo(clientAttribute.get("longKey"));
assertThat(attributes3.get("client").get("stringKey")).isEqualTo(clientAttribute.get("stringKey"));
}
}

226
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java

@ -16,7 +16,6 @@
package org.thingsboard.server.msa.connectivity;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
@ -26,19 +25,19 @@ import io.netty.buffer.Unpooled;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig;
import org.thingsboard.mqtt.MqttHandler;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@ -61,14 +60,29 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.testng.Assert.fail;
import static org.thingsboard.server.common.data.DataConstants.DEVICE;
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultDevicePrototype;
@Slf4j
public class MqttClientTest extends AbstractContainerTest {
private Device device;
@BeforeMethod
public void setUp() throws Exception {
testRestClient.login("tenant@thingsboard.org", "tenant");
device = testRestClient.postDevice("", defaultDevicePrototype("http_"));
}
@AfterMethod
public void tearDown() {
testRestClient.deleteDeviceIfExists(device.getId());
}
@Test
public void telemetryUpload() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
MqttClient mqttClient = getMqttClient(deviceCredentials, null);
@ -77,25 +91,19 @@ public class MqttClientTest extends AbstractContainerTest {
log.info("Received telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size());
Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"),
actualLatestTelemetry.getLatestValues().keySet());
assertThat(actualLatestTelemetry.getData()).hasSize(4);
assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey"));
Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString()));
Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1"));
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0)));
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73)));
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
}
@Test
public void telemetryUploadWithTs() throws Exception {
long ts = 1451649600512L;
restClient.login("tenant@thingsboard.org", "tenant");
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
WsClient wsClient = subscribeToWebSocket(device.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
MqttClient mqttClient = getMqttClient(deviceCredentials, null);
@ -104,22 +112,18 @@ public class MqttClientTest extends AbstractContainerTest {
log.info("Received telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size());
Assert.assertEquals(getExpectedLatestValues(ts), actualLatestTelemetry.getLatestValues());
assertThat(actualLatestTelemetry.getData()).hasSize(4);
assertThat(getExpectedLatestValues(ts)).isEqualTo(actualLatestTelemetry.getLatestValues());
Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", ts, Boolean.TRUE.toString()));
Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", ts, "value1"));
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", ts, Double.toString(42.0)));
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", ts, Long.toString(73)));
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
}
@Test
public void publishAttributeUpdateToServer() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS);
MqttMessageListener listener = new MqttMessageListener();
@ -134,23 +138,18 @@ public class MqttClientTest extends AbstractContainerTest {
log.info("Received telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size());
Assert.assertEquals(Sets.newHashSet("attr1", "attr2", "attr3", "attr4"),
actualLatestTelemetry.getLatestValues().keySet());
Assert.assertTrue(verify(actualLatestTelemetry, "attr1", "value1"));
Assert.assertTrue(verify(actualLatestTelemetry, "attr2", Boolean.TRUE.toString()));
Assert.assertTrue(verify(actualLatestTelemetry, "attr3", Double.toString(42.0)));
Assert.assertTrue(verify(actualLatestTelemetry, "attr4", Long.toString(73)));
assertThat(actualLatestTelemetry.getData()).hasSize(4);
assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("attr1", "attr2", "attr3", "attr4"));
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
assertThat(actualLatestTelemetry.getDataValuesByKey("attr1").get(1)).isEqualTo("value1");
assertThat(actualLatestTelemetry.getDataValuesByKey("attr2").get(1)).isEqualTo(Boolean.TRUE.toString());
assertThat(actualLatestTelemetry.getDataValuesByKey("attr3").get(1)).isEqualTo(Double.toString(42.0));
assertThat(actualLatestTelemetry.getDataValuesByKey("attr4").get(1)).isEqualTo(Long.toString(73));
}
@Test
public void requestAttributeValuesFromServer() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
WsClient wsClient = subscribeToWebSocket(device.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS);
MqttMessageListener listener = new MqttMessageListener();
@ -166,21 +165,16 @@ public class MqttClientTest extends AbstractContainerTest {
log.info("Received ws telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking();
Assert.assertEquals(1, actualLatestTelemetry.getData().size());
Assert.assertEquals(Sets.newHashSet("clientAttr"),
actualLatestTelemetry.getLatestValues().keySet());
Assert.assertTrue(verify(actualLatestTelemetry, "clientAttr", clientAttributeValue));
assertThat(actualLatestTelemetry.getData()).hasSize(1);
assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnly("clientAttr");
assertThat(actualLatestTelemetry.getDataValuesByKey("clientAttr").get(1)).isEqualTo(clientAttributeValue);
// Add a new shared attribute
JsonObject sharedAttributes = new JsonObject();
String sharedAttributeValue = StringUtils.randomAlphanumeric(8);
sharedAttributes.addProperty("sharedAttr", sharedAttributeValue);
ResponseEntity sharedAttributesResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
device.getId());
Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
JsonNode sharedAttribute = mapper.readTree(sharedAttributes.toString());
testRestClient.postTelemetryAttribute(DataConstants.DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute);
// Subscribe to attributes response
mqttClient.on("v1/devices/me/attributes/response/+", listener, MqttQoS.AT_LEAST_ONCE).get();
@ -197,20 +191,16 @@ public class MqttClientTest extends AbstractContainerTest {
AttributesResponse attributes = mapper.readValue(Objects.requireNonNull(event).getMessage(), AttributesResponse.class);
log.info("Received telemetry: {}", attributes);
Assert.assertEquals(1, attributes.getClient().size());
Assert.assertEquals(clientAttributeValue, attributes.getClient().get("clientAttr"));
Assert.assertEquals(1, attributes.getShared().size());
Assert.assertEquals(sharedAttributeValue, attributes.getShared().get("sharedAttr"));
assertThat(attributes.getClient()).hasSize(1);
assertThat(attributes.getClient().get("clientAttr")).isEqualTo(clientAttributeValue);
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
assertThat(attributes.getShared()).hasSize(1);
assertThat(attributes.getShared().get("sharedAttr")).isEqualTo(sharedAttributeValue);
}
@Test
public void subscribeToAttributeUpdatesFromServer() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
@ -225,38 +215,28 @@ public class MqttClientTest extends AbstractContainerTest {
JsonObject sharedAttributes = new JsonObject();
String sharedAttributeValue = StringUtils.randomAlphanumeric(8);
sharedAttributes.addProperty(sharedAttributeName, sharedAttributeValue);
ResponseEntity sharedAttributesResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
device.getId());
Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
JsonNode sharedAttribute = mapper.readTree(sharedAttributes.toString());
testRestClient.postTelemetryAttribute(DataConstants.DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute);
MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertEquals(sharedAttributeValue,
mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText());
assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText())
.isEqualTo(sharedAttributeValue);
// Update the shared attribute value
JsonObject updatedSharedAttributes = new JsonObject();
String updatedSharedAttributeValue = StringUtils.randomAlphanumeric(8);
updatedSharedAttributes.addProperty(sharedAttributeName, updatedSharedAttributeValue);
ResponseEntity updatedSharedAttributesResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(updatedSharedAttributes.toString()), ResponseEntity.class,
device.getId());
Assert.assertTrue(updatedSharedAttributesResponse.getStatusCode().is2xxSuccessful());
testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString()));
event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertEquals(updatedSharedAttributeValue,
mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText());
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText())
.isEqualTo(updatedSharedAttributeValue);
}
@Test
public void serverSideRpc() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
@ -270,21 +250,18 @@ public class MqttClientTest extends AbstractContainerTest {
serverRpcPayload.addProperty("method", "getValue");
serverRpcPayload.addProperty("params", true);
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName())));
ListenableFuture<ResponseEntity> future = service.submit(() -> {
ListenableFuture<JsonNode> future = service.submit(() -> {
try {
return restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/rpc/twoway/{deviceId}",
mapper.readTree(serverRpcPayload.toString()), String.class,
device.getId());
return testRestClient.postServerSideRpc(device.getId(), mapper.readTree(serverRpcPayload.toString()));
} catch (IOException e) {
return ResponseEntity.badRequest().build();
return null;
}
});
// Wait for RPC call from the server and send the response
MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertEquals("{\"method\":\"getValue\",\"params\":true}", Objects.requireNonNull(requestFromServer).getMessage());
assertThat(Objects.requireNonNull(requestFromServer).getMessage()).isEqualTo("{\"method\":\"getValue\",\"params\":true}");
Integer requestId = Integer.valueOf(Objects.requireNonNull(requestFromServer).getTopic().substring("v1/devices/me/rpc/request/".length()));
JsonObject clientResponse = new JsonObject();
@ -292,19 +269,14 @@ public class MqttClientTest extends AbstractContainerTest {
// Send a response to the server's RPC request
mqttClient.publish("v1/devices/me/rpc/response/" + requestId, Unpooled.wrappedBuffer(clientResponse.toString().getBytes())).get();
ResponseEntity serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS);
JsonNode serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS);
service.shutdownNow();
Assert.assertTrue(serverResponse.getStatusCode().is2xxSuccessful());
Assert.assertEquals(clientResponse.toString(), serverResponse.getBody());
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
assertThat(serverResponse).isEqualTo(mapper.readTree(clientResponse.toString()));
}
@Test
public void clientSideRpc() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
Device device = createDevice("mqtt_");
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
@ -328,46 +300,33 @@ public class MqttClientTest extends AbstractContainerTest {
TimeUnit.SECONDS.sleep(1 * timeoutMultiplier);
MqttEvent responseFromServer = listener.getEvents().poll(1 * timeoutMultiplier, TimeUnit.SECONDS);
Integer responseId = Integer.valueOf(Objects.requireNonNull(responseFromServer).getTopic().substring("v1/devices/me/rpc/response/".length()));
Assert.assertEquals(requestId, responseId);
Assert.assertEquals("requestReceived", mapper.readTree(responseFromServer.getMessage()).get("response").asText());
assertThat(responseId).isEqualTo(requestId);
assertThat(mapper.readTree(responseFromServer.getMessage()).get("response").asText()).isEqualTo("requestReceived");
// Make the default rule chain a root again
ResponseEntity<RuleChain> rootRuleChainResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/ruleChain/{ruleChainId}/root",
null,
RuleChain.class,
defaultRuleChainId);
Assert.assertTrue(rootRuleChainResponse.getStatusCode().is2xxSuccessful());
testRestClient.setRootRuleChain(defaultRuleChainId);
// Delete the created rule chain
restClient.getRestTemplate().delete(HTTPS_URL + "/api/ruleChain/{ruleChainId}", ruleChainId);
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
testRestClient.deleteRuleChain(ruleChainId);
}
@Test
public void deviceDeletedClosingSession() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
String deviceForDeletingTestName = "Device for deleting notification test";
Device device = createDevice(deviceForDeletingTestName);
DeviceCredentials deviceCredentials = restClient.getDeviceCredentialsByDeviceId(device.getId()).get();
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener);
restClient.deleteDevice(device.getId());
testRestClient.deleteDeviceIfExists(device.getId());
TimeUnit.SECONDS.sleep(3 * timeoutMultiplier);
Assert.assertFalse(mqttClient.isConnected());
assertThat(mqttClient.isConnected()).isFalse();
}
private RuleChainId createRootRuleChainForRpcResponse() throws Exception {
RuleChain newRuleChain = new RuleChain();
newRuleChain.setName("testRuleChain");
ResponseEntity<RuleChain> ruleChainResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/ruleChain",
newRuleChain,
RuleChain.class);
Assert.assertTrue(ruleChainResponse.getStatusCode().is2xxSuccessful());
RuleChain ruleChain = ruleChainResponse.getBody();
RuleChain ruleChain = testRestClient.postRootRuleChain(newRuleChain);
JsonNode configuration = mapper.readTree(this.getClass().getClassLoader().getResourceAsStream("RpcResponseRuleChainMetadata.json"));
RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
@ -376,37 +335,22 @@ public class MqttClientTest extends AbstractContainerTest {
ruleChainMetaData.setNodes(Arrays.asList(mapper.treeToValue(configuration.get("nodes"), RuleNode[].class)));
ruleChainMetaData.setConnections(Arrays.asList(mapper.treeToValue(configuration.get("connections"), NodeConnectionInfo[].class)));
ResponseEntity<RuleChainMetaData> ruleChainMetadataResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/ruleChain/metadata",
ruleChainMetaData,
RuleChainMetaData.class);
Assert.assertTrue(ruleChainMetadataResponse.getStatusCode().is2xxSuccessful());
testRestClient.postRuleChainMetadata(ruleChainMetaData);
// Set a new rule chain as root
ResponseEntity<RuleChain> rootRuleChainResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/ruleChain/{ruleChainId}/root",
null,
RuleChain.class,
ruleChain.getId());
Assert.assertTrue(rootRuleChainResponse.getStatusCode().is2xxSuccessful());
testRestClient.setRootRuleChain(ruleChain.getId());
return ruleChain.getId();
}
private RuleChainId getDefaultRuleChainId() {
ResponseEntity<PageData<RuleChain>> ruleChains = restClient.getRestTemplate().exchange(
HTTPS_URL + "/api/ruleChains?pageSize=40&page=0&textSearch=",
HttpMethod.GET,
null,
new ParameterizedTypeReference<PageData<RuleChain>>() {
});
Optional<RuleChain> defaultRuleChain = ruleChains.getBody().getData()
PageData<RuleChain> ruleChains = testRestClient.getRuleChains(new PageLink(40, 0));
Optional<RuleChain> defaultRuleChain = ruleChains.getData()
.stream()
.filter(RuleChain::isRoot)
.findFirst();
if (!defaultRuleChain.isPresent()) {
Assert.fail("Root rule chain wasn't found");
fail("Root rule chain wasn't found");
}
return defaultRuleChain.get().getId();
}

220
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java

@ -16,7 +16,6 @@
package org.thingsboard.server.msa.connectivity;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
@ -28,15 +27,17 @@ import io.netty.buffer.Unpooled;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.mqtt.MqttClient;
import org.thingsboard.mqtt.MqttClientConfig;
import org.thingsboard.mqtt.MqttHandler;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.DeviceId;
@ -50,9 +51,9 @@ import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
@ -60,28 +61,34 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.thingsboard.server.common.data.DataConstants.DEVICE;
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
import static org.thingsboard.server.msa.prototypes.DevicePrototypes.defaultGatewayPrototype;
@Slf4j
public class MqttGatewayClientTest extends AbstractContainerTest {
Device gatewayDevice;
MqttClient mqttClient;
Device createdDevice;
MqttMessageListener listener;
private Device gatewayDevice;
private MqttClient mqttClient;
private Device createdDevice;
private MqttMessageListener listener;
private JsonParser jsonParser = new JsonParser();
@Before
@BeforeMethod
public void createGateway() throws Exception {
restClient.login("tenant@thingsboard.org", "tenant");
this.gatewayDevice = createGatewayDevice();
Optional<DeviceCredentials> gatewayDeviceCredentials = restClient.getDeviceCredentialsByDeviceId(gatewayDevice.getId());
Assert.assertTrue(gatewayDeviceCredentials.isPresent());
testRestClient.login("tenant@thingsboard.org", "tenant");
gatewayDevice = testRestClient.postDevice("", defaultGatewayPrototype());
DeviceCredentials gatewayDeviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(gatewayDevice.getId());
this.listener = new MqttMessageListener();
this.mqttClient = getMqttClient(gatewayDeviceCredentials.get(), listener);
this.mqttClient = getMqttClient(gatewayDeviceCredentials, listener);
this.createdDevice = createDeviceThroughGateway(mqttClient, gatewayDevice);
}
@After
public void removeGateway() throws Exception {
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + this.gatewayDevice.getId());
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + this.createdDevice.getId());
@AfterMethod
public void removeGateway() {
testRestClient.deleteDeviceIfExists(this.gatewayDevice.getId());
testRestClient.deleteDeviceIfExists(this.createdDevice.getId());
this.listener = null;
this.mqttClient = null;
this.createdDevice = null;
@ -95,40 +102,38 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
log.info("Received telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size());
Assert.assertEquals(Sets.newHashSet("booleanKey", "stringKey", "doubleKey", "longKey"),
actualLatestTelemetry.getLatestValues().keySet());
assertThat(actualLatestTelemetry.getData()).hasSize(4);
assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey"));
Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", Boolean.TRUE.toString()));
Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", "value1"));
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0)));
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73)));
assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
}
@Test
public void telemetryUploadWithTs() throws Exception {
long ts = 1451649600512L;
restClient.login("tenant@thingsboard.org", "tenant");
WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "LATEST_TELEMETRY", CmdsType.TS_SUB_CMDS);
mqttClient.publish("v1/gateway/telemetry", Unpooled.wrappedBuffer(createGatewayPayload(createdDevice.getName(), ts).toString().getBytes())).get();
WsTelemetryResponse actualLatestTelemetry = wsClient.getLastMessage();
log.info("Received telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size());
Assert.assertEquals(getExpectedLatestValues(ts), actualLatestTelemetry.getLatestValues());
assertThat(actualLatestTelemetry.getData()).hasSize(4);
assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("booleanKey", "stringKey", "doubleKey", "longKey"));
Assert.assertTrue(verify(actualLatestTelemetry, "booleanKey", ts, Boolean.TRUE.toString()));
Assert.assertTrue(verify(actualLatestTelemetry, "stringKey", ts, "value1"));
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", ts, Double.toString(42.0)));
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", ts, Long.toString(73)));
assertThat(actualLatestTelemetry.getDataValuesByKey("booleanKey").get(1)).isEqualTo(Boolean.TRUE.toString());
assertThat(actualLatestTelemetry.getDataValuesByKey("stringKey").get(1)).isEqualTo("value1");
assertThat(actualLatestTelemetry.getDataValuesByKey("doubleKey").get(1)).isEqualTo(Double.toString(42.0));
assertThat(actualLatestTelemetry.getDataValuesByKey("longKey").get(1)).isEqualTo(Long.toString(73));
}
@Test
public void publishAttributeUpdateToServer() throws Exception {
Optional<DeviceCredentials> createdDeviceCredentials = restClient.getDeviceCredentialsByDeviceId(createdDevice.getId());
Assert.assertTrue(createdDeviceCredentials.isPresent());
testRestClient.getDeviceCredentialsByDeviceId(createdDevice.getId());
WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS);
JsonObject clientAttributes = new JsonObject();
clientAttributes.addProperty("attr1", "value1");
@ -142,20 +147,18 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
log.info("Received attributes: {}", actualLatestTelemetry);
wsClient.closeBlocking();
Assert.assertEquals(4, actualLatestTelemetry.getData().size());
Assert.assertEquals(Sets.newHashSet("attr1", "attr2", "attr3", "attr4"),
actualLatestTelemetry.getLatestValues().keySet());
assertThat(actualLatestTelemetry.getData()).hasSize(4);
assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnlyOnceElementsOf(Arrays.asList("attr1", "attr2", "attr3", "attr4"));
Assert.assertTrue(verify(actualLatestTelemetry, "attr1", "value1"));
Assert.assertTrue(verify(actualLatestTelemetry, "attr2", Boolean.TRUE.toString()));
Assert.assertTrue(verify(actualLatestTelemetry, "attr3", Double.toString(42.0)));
Assert.assertTrue(verify(actualLatestTelemetry, "attr4", Long.toString(73)));
assertThat(actualLatestTelemetry.getDataValuesByKey("attr1").get(1)).isEqualTo("value1");
assertThat(actualLatestTelemetry.getDataValuesByKey("attr2").get(1)).isEqualTo(Boolean.TRUE.toString());
assertThat(actualLatestTelemetry.getDataValuesByKey("attr3").get(1)).isEqualTo(Double.toString(42.0));
assertThat(actualLatestTelemetry.getDataValuesByKey("attr4").get(1)).isEqualTo(Long.toString(73));
}
@Test
public void responseDataOnAttributesRequestCheck() throws Exception {
Optional<DeviceCredentials> createdDeviceCredentials = restClient.getDeviceCredentialsByDeviceId(createdDevice.getId());
Assert.assertTrue(createdDeviceCredentials.isPresent());
testRestClient.getDeviceCredentialsByDeviceId(createdDevice.getId());
JsonObject sharedAttributes = new JsonObject();
sharedAttributes.addProperty("attr1", "value1");
sharedAttributes.addProperty("attr2", true);
@ -163,11 +166,8 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
sharedAttributes.addProperty("attr4", 73);
mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get();
ResponseEntity sharedAttributesResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
createdDevice.getId());
Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
testRestClient.postTelemetryAttribute(DataConstants.DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString()));
var event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
JsonObject requestData = new JsonObject();
@ -181,8 +181,8 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
JsonObject responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject();
Assert.assertTrue(responseData.has("value"));
Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("value").getAsString());
assertThat(responseData.has("value")).isTrue();
assertThat(responseData.get("value").getAsString()).isEqualTo(sharedAttributes.get("attr1").getAsString());
requestData = new JsonObject();
requestData.addProperty("id", 1);
@ -198,9 +198,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject();
Assert.assertTrue(responseData.has("values"));
Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("values").getAsJsonObject().get("attr1").getAsString());
Assert.assertEquals(sharedAttributes.get("attr2").getAsString(), responseData.get("values").getAsJsonObject().get("attr2").getAsString());
assertThat(responseData.has("values")).isTrue();
assertThat(responseData.get("values").getAsJsonObject().get("attr1").getAsString()).isEqualTo(sharedAttributes.get("attr1").getAsString());
assertThat(responseData.get("values").getAsJsonObject().get("attr2").getAsString()).isEqualTo(sharedAttributes.get("attr2").getAsString());
requestData = new JsonObject();
requestData.addProperty("id", 1);
@ -216,9 +216,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
responseData = jsonParser.parse(Objects.requireNonNull(event).getMessage()).getAsJsonObject();
Assert.assertTrue(responseData.has("values"));
Assert.assertEquals(sharedAttributes.get("attr1").getAsString(), responseData.get("values").getAsJsonObject().get("attr1").getAsString());
Assert.assertEquals(1, responseData.get("values").getAsJsonObject().entrySet().size());
assertThat(responseData.has("values")).isTrue();
assertThat(responseData.get("values").getAsJsonObject().get("attr1").getAsString()).isEqualTo(sharedAttributes.get("attr1").getAsString());
assertThat(responseData.get("values").getAsJsonObject().entrySet()).hasSize(1);
}
@Test
@ -237,11 +237,9 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
log.info("Received ws telemetry: {}", actualLatestTelemetry);
wsClient.closeBlocking();
Assert.assertEquals(1, actualLatestTelemetry.getData().size());
Assert.assertEquals(Sets.newHashSet("clientAttr"),
actualLatestTelemetry.getLatestValues().keySet());
Assert.assertTrue(verify(actualLatestTelemetry, "clientAttr", clientAttributeValue));
assertThat(actualLatestTelemetry.getData()).hasSize(1);
assertThat(actualLatestTelemetry.getLatestValues().keySet()).containsOnly("clientAttr");
assertThat(actualLatestTelemetry.getDataValuesByKey("clientAttr").get(1)).isEqualTo(clientAttributeValue);
// Add a new shared attribute
JsonObject sharedAttributes = new JsonObject();
@ -251,16 +249,12 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
// Subscribe for attribute update event
mqttClient.on("v1/gateway/attributes", listener, MqttQoS.AT_LEAST_ONCE).get();
ResponseEntity sharedAttributesResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
createdDevice.getId());
Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString()));
MqttEvent sharedAttributeEvent = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
// Catch attribute update event
Assert.assertNotNull(sharedAttributeEvent);
Assert.assertEquals("v1/gateway/attributes", sharedAttributeEvent.getTopic());
assertThat(sharedAttributeEvent).isNotNull();
assertThat(sharedAttributeEvent.getTopic()).isEqualTo("v1/gateway/attributes");
// Subscribe to attributes response
mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get();
@ -288,15 +282,11 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
gatewaySharedAttributeValue.addProperty("device", createdDevice.getName());
gatewaySharedAttributeValue.add("data", sharedAttributes);
ResponseEntity sharedAttributesResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(sharedAttributes.toString()), ResponseEntity.class,
createdDevice.getId());
Assert.assertTrue(sharedAttributesResponse.getStatusCode().is2xxSuccessful());
testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString()));
MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertEquals(sharedAttributeValue,
mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText());
assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText())
.isEqualTo(sharedAttributeValue);
// Update the shared attribute value
JsonObject updatedSharedAttributes = new JsonObject();
@ -307,15 +297,10 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
gatewayUpdatedSharedAttributeValue.addProperty("device", createdDevice.getName());
gatewayUpdatedSharedAttributeValue.add("data", updatedSharedAttributes);
ResponseEntity updatedSharedAttributesResponse = restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE",
mapper.readTree(updatedSharedAttributes.toString()), ResponseEntity.class,
createdDevice.getId());
Assert.assertTrue(updatedSharedAttributesResponse.getStatusCode().is2xxSuccessful());
testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString()));
event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertEquals(updatedSharedAttributeValue,
mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText());
assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText())
.isEqualTo(updatedSharedAttributeValue);
}
@Test
@ -331,14 +316,11 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
serverRpcPayload.addProperty("method", "getValue");
serverRpcPayload.addProperty("params", true);
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName(getClass().getSimpleName())));
ListenableFuture<ResponseEntity> future = service.submit(() -> {
ListenableFuture<JsonNode> future = service.submit(() -> {
try {
return restClient.getRestTemplate()
.postForEntity(HTTPS_URL + "/api/rpc/twoway/{deviceId}",
mapper.readTree(serverRpcPayload.toString()), String.class,
createdDevice.getId());
return testRestClient.postServerSideRpc(createdDevice.getId(), mapper.readTree(serverRpcPayload.toString()));
} catch (IOException e) {
return ResponseEntity.badRequest().build();
return null;
}
});
@ -346,19 +328,13 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
MqttEvent requestFromServer = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
service.shutdownNow();
Assert.assertNotNull(requestFromServer);
Assert.assertNotNull(requestFromServer.getMessage());
JsonObject requestFromServerJson = new JsonParser().parse(requestFromServer.getMessage()).getAsJsonObject();
Assert.assertEquals(createdDevice.getName(), requestFromServerJson.get("device").getAsString());
JsonObject requestFromServerData = requestFromServerJson.get("data").getAsJsonObject();
Assert.assertEquals("getValue", requestFromServerData.get("method").getAsString());
Assert.assertTrue(requestFromServerData.get("params").getAsBoolean());
int requestId = requestFromServerData.get("id").getAsInt();
assertThat(requestFromServer).isNotNull();
assertThat(requestFromServer.getMessage()).isNotNull();
JsonNode requestFromServerJson = JacksonUtil.toJsonNode(requestFromServer.getMessage());
assertThat(requestFromServerJson.get("device").asText()).isEqualTo(createdDevice.getName());
assertThat(requestFromServerJson.get("data").get("method").asText()).isEqualTo("getValue");
assertThat(requestFromServerJson.get("data").get("params").asText()).isEqualTo("true");
int requestId = requestFromServerJson.get("data").get("id").asInt();
JsonObject clientResponse = new JsonObject();
clientResponse.addProperty("response", "someResponse");
@ -369,16 +345,15 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
// Send a response to the server's RPC request
mqttClient.publish(gatewayRpcTopic, Unpooled.wrappedBuffer(gatewayResponse.toString().getBytes())).get();
ResponseEntity serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertTrue(serverResponse.getStatusCode().is2xxSuccessful());
Assert.assertEquals(clientResponse.toString(), serverResponse.getBody());
JsonNode serverResponse = future.get(5 * timeoutMultiplier, TimeUnit.SECONDS);
assertThat(serverResponse).isEqualTo(mapper.readTree(clientResponse.toString()));
}
@Test
public void deviceCreationAfterDeleted() throws Exception {
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + this.createdDevice.getId());
Optional<Device> deletedDevice = restClient.getDeviceById(this.createdDevice.getId());
Assert.assertTrue(deletedDevice.isEmpty());
testRestClient.deleteDevice(this.createdDevice.getId());
testRestClient.getDeviceById(this.createdDevice.getId(), HttpStatus.NOT_FOUND.value());
this.createdDevice = createDeviceThroughGateway(mqttClient, gatewayDevice);
}
@ -397,13 +372,13 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
log.info(gatewayAttributesRequest.toString());
mqttClient.publish("v1/gateway/attributes/request", Unpooled.wrappedBuffer(gatewayAttributesRequest.toString().getBytes())).get();
MqttEvent clientAttributeEvent = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS);
Assert.assertNotNull(clientAttributeEvent);
assertThat(clientAttributeEvent).isNotNull();
JsonObject responseMessage = new JsonParser().parse(Objects.requireNonNull(clientAttributeEvent).getMessage()).getAsJsonObject();
Assert.assertEquals(messageId, responseMessage.get("id").getAsInt());
Assert.assertEquals(createdDevice.getName(), responseMessage.get("device").getAsString());
Assert.assertEquals(3, responseMessage.entrySet().size());
Assert.assertEquals(expectedValue, responseMessage.get("value").getAsString());
assertThat(responseMessage.get("id").getAsInt()).isEqualTo(messageId);
assertThat(responseMessage.get("device").getAsString()).isEqualTo(createdDevice.getName());
assertThat(responseMessage.entrySet()).hasSize(3);
assertThat(responseMessage.get("value").getAsString()).isEqualTo(expectedValue);
}
private Device createDeviceThroughGateway(MqttClient mqttClient, Device gatewayDevice) throws Exception {
@ -411,24 +386,19 @@ public class MqttGatewayClientTest extends AbstractContainerTest {
TimeUnit.SECONDS.sleep(30);
}
String deviceName = "mqtt_device";
String deviceName = "mqtt_device" + RandomStringUtils.randomAlphabetic(5);
mqttClient.publish("v1/gateway/connect", Unpooled.wrappedBuffer(createGatewayConnectPayload(deviceName).toString().getBytes()), MqttQoS.AT_LEAST_ONCE).get();
if (timeoutMultiplier > 1) {
TimeUnit.SECONDS.sleep(30);
}
List<EntityRelation> relations = restClient.findByFrom(gatewayDevice.getId(), RelationTypeGroup.COMMON);
Assert.assertEquals(1, relations.size());
List<EntityRelation> relations = testRestClient.findRelationByFrom(gatewayDevice.getId(), RelationTypeGroup.COMMON);
assertThat(relations).hasSize(1);
EntityId createdEntityId = relations.get(0).getTo();
DeviceId createdDeviceId = new DeviceId(createdEntityId.getId());
Optional<Device> createdDevice = restClient.getDeviceById(createdDeviceId);
Assert.assertTrue(createdDevice.isPresent());
return createdDevice.get();
return testRestClient.getDeviceById(createdDeviceId);
}
private MqttClient getMqttClient(DeviceCredentials deviceCredentials, MqttMessageListener listener) throws InterruptedException, ExecutionException {

40
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/prototypes/DevicePrototypes.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2022 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.prototypes;
import com.fasterxml.jackson.databind.JsonNode;
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
public class DevicePrototypes {
public static Device defaultDevicePrototype(String name){
Device device = new Device();
device.setName(name + RandomStringUtils.randomAlphanumeric(7));
device.setType("DEFAULT");
return device;
}
public static Device defaultGatewayPrototype() {
String isGateway = "{\"gateway\":true}";
JsonNode additionalInfo = JacksonUtil.toJsonNode(isGateway);
Device gatewayDeviceTemplate = new Device();
gatewayDeviceTemplate.setName("mqtt_gateway_" + RandomStringUtils.randomAlphanumeric(5));
gatewayDeviceTemplate.setType("gateway");
gatewayDeviceTemplate.setAdditionalInfo(additionalInfo);
return gatewayDeviceTemplate;
}
}

2
msa/black-box-tests/src/test/resources/config.properties

@ -0,0 +1,2 @@
tb.baseUrl=http://localhost:8080
tb.wsUrl=ws://localhost:8080

27
msa/black-box-tests/src/test/resources/testNG.xml

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
Copyright © 2016-2022 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.
-->
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Black-box tests">
<test verbose="2" name="Connectivity tests" preserve-order="false">
<packages>
<package name="org.thingsboard.server.msa.connectivity" />
</packages>
</test>
</suite>

2
msa/js-executor/api/jsExecutor.models.ts

@ -56,7 +56,7 @@ export interface JsCompileResponse extends TbMessage {
export interface JsInvokeResponse {
success: boolean;
result: string;
result?: string;
errorCode?: number;
errorDetails?: string;
}

11
msa/js-executor/api/jsInvokeMessageProcessor.ts

@ -39,6 +39,7 @@ const TIMEOUT_ERROR = 2;
const NOT_FOUND_ERROR = 3;
const statFrequency = Number(config.get('script.stat_print_frequency'));
const memoryUsageTraceFrequency = Number(config.get('script.memory_usage_trace_frequency'));
const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency'));
const useSandbox = config.get('script.use_sandbox') === 'true';
const maxActiveScripts = Number(config.get('script.max_active_scripts'));
@ -167,11 +168,15 @@ export class JsInvokeMessageProcessor {
if (this.executedScriptsCounter % scriptBodyTraceFrequency == 0) {
this.logger.info('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody);
}
if (this.executedScriptsCounter % memoryUsageTraceFrequency == 0) {
this.logger.info('Current memory usage: [%s]', process.memoryUsage());
}
this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then(
(script) => {
this.executor.executeScript(script, invokeRequest.args, invokeRequest.timeout).then(
(result) => {
if (result.length <= maxResultSize) {
(result: string | undefined) => {
if (!result || result.length <= maxResultSize) {
const invokeResponse = JsInvokeMessageProcessor.createInvokeResponse(result, true);
this.logger.debug('[%s] Sending success invoke response, scriptId: [%s]', requestId, scriptId);
this.sendResponse(requestId, responseTopic, headers, scriptId, undefined, invokeResponse);
@ -323,7 +328,7 @@ export class JsInvokeMessageProcessor {
}
}
private static createInvokeResponse(result: string, success: boolean, errorCode?: number, err?: any): JsInvokeResponse {
private static createInvokeResponse(result: string | undefined, success: boolean, errorCode?: number, err?: any): JsInvokeResponse {
return {
errorCode: errorCode,
success: success,

1
msa/js-executor/config/custom-environment-variables.yml

@ -75,6 +75,7 @@ logger:
script:
use_sandbox: "SCRIPT_USE_SANDBOX"
memory_usage_trace_frequency: "MEMORY_USAGE_TRACE_FREQUENCY"
stat_print_frequency: "SCRIPT_STAT_PRINT_FREQUENCY"
script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY"
max_active_scripts: "MAX_ACTIVE_SCRIPTS"

1
msa/js-executor/config/default.yml

@ -64,6 +64,7 @@ logger:
script:
use_sandbox: "true"
memory_usage_trace_frequency: "1000"
script_body_trace_frequency: "10000"
stat_print_frequency: "10000"
max_active_scripts: "1000"

2
msa/js-executor/docker/start-js-executor.sh

@ -27,4 +27,4 @@ source "${CONF_FOLDER}/${configfile}"
cd ${pkg.installFolder}
# This will forward this PID 1 to the node.js and forward SIGTERM for graceful shutdown as well
exec node server.js
exec node --no-compilation-cache server.js

4
packaging/java/scripts/install/logback.xml

@ -56,6 +56,10 @@
<appender-ref ref="STDOUT" />
</logger>
<logger name="org.thingsboard.server.config.jwt" level="INFO">
<appender-ref ref="STDOUT" />
</logger>
<logger name="org.thingsboard.server" level="INFO" />
<root level="INFO">

34
pom.xml

@ -77,7 +77,7 @@
<zookeeper.version>3.5.5</zookeeper.version>
<protobuf.version>3.21.9</protobuf.version>
<grpc.version>1.42.1</grpc.version>
<mvel.version>2.4.23TB</mvel.version>
<tbel.version>1.0.0</tbel.version>
<lombok.version>1.18.18</lombok.version>
<paho.client.version>1.2.4</paho.client.version>
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>
@ -135,6 +135,10 @@
<spring-test-dbunit.version>1.3.0</spring-test-dbunit.version> <!-- 2016 -->
<takari-cpsuite.version>1.2.7</takari-cpsuite.version> <!-- 2015 -->
<!-- BLACKBOX TEST SCOPE -->
<testng.version>7.6.1</testng.version>
<assertj.version>3.23.1</assertj.version>
<rest-assured.version>5.2.0</rest-assured.version>
<hamcrest.version>1.3</hamcrest.version>
<testcontainers.version>1.17.3</testcontainers.version>
<zeroturnaround.version>1.12</zeroturnaround.version>
<opensmpp.version>3.0.0</opensmpp.version>
@ -1572,8 +1576,8 @@
</dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>mvel2</artifactId>
<version>${mvel.version}</version>
<artifactId>tbel</artifactId>
<version>${tbel.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
@ -1635,6 +1639,30 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>

2
pull_request_template.md

@ -12,7 +12,7 @@ Put your PR description here instead of this sentence.
- [ ] Description contains brief notes about what needs to be added to the documentation.
- [ ] No merge conflicts, commented blocks of code, code formatting issues.
- [ ] Changes are backward compatible or upgrade script is provided.
- [ ] Similar PR is opened for PE version to simplify merge. Required for internal contributors only.
- [ ] Similar PR is opened for PE version to simplify merge. Crosslinks between PRs added. Required for internal contributors only.
## Front-End feature checklist

26
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -136,6 +136,8 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.common.data.sms.config.TestSmsRequest;
@ -286,6 +288,23 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
return restTemplate.postForEntity(baseURL + "/api/admin/securitySettings", securitySettings, SecuritySettings.class).getBody();
}
public Optional<JwtSettings> getJwtSettings() {
try {
ResponseEntity<JwtSettings> jwtSettings = restTemplate.getForEntity(baseURL + "/api/admin/jwtSettings", JwtSettings.class);
return Optional.ofNullable(jwtSettings.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
return Optional.empty();
} else {
throw exception;
}
}
}
public JwtPair saveJwtSettings(JwtSettings jwtSettings) {
return restTemplate.postForEntity(baseURL + "/api/admin/jwtSettings", jwtSettings, JwtPair.class).getBody();
}
public Optional<RepositorySettings> getRepositorySettings() {
try {
ResponseEntity<RepositorySettings> repositorySettings = restTemplate.getForEntity(baseURL + "/api/admin/repositorySettings", RepositorySettings.class);
@ -2032,10 +2051,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
public PageData<RuleChain> getRuleChains(PageLink pageLink) {
return getRuleChains(RuleChainType.CORE, pageLink);
}
public PageData<RuleChain> getRuleChains(RuleChainType ruleChainType, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("type", ruleChainType.name());
addPageLinkToParam(params, pageLink);
return restTemplate.exchange(
baseURL + "/api/ruleChains?" + getUrlParams(pageLink),
baseURL + "/api/ruleChains?type={type}&" + getUrlParams(pageLink),
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<PageData<RuleChain>>() {

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java

@ -48,7 +48,7 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadAlarmNodeConfig(configuration);
scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getAlarmDetailsBuildMvel() : config.getAlarmDetailsBuildJs());
ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getAlarmDetailsBuildTbel() : config.getAlarmDetailsBuildJs());
}
protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException;

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java

@ -33,7 +33,7 @@ public abstract class TbAbstractAlarmNodeConfiguration {
"\n" +
"return details;";
static final String ALARM_DETAILS_BUILD_MVEL_TEMPLATE = "" +
static final String ALARM_DETAILS_BUILD_TBEL_TEMPLATE = "" +
"var details = {};\n" +
"if (metadata.prevAlarmDetails != null) {\n" +
" details = JSON.parse(metadata.prevAlarmDetails);\n" +
@ -49,6 +49,6 @@ public abstract class TbAbstractAlarmNodeConfiguration {
private String alarmType;
private ScriptLanguage scriptLang;
private String alarmDetailsBuildJs;
private String alarmDetailsBuildMvel;
private String alarmDetailsBuildTbel;
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.action;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data
@ -26,9 +25,9 @@ public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigurat
@Override
public TbClearAlarmNodeConfiguration defaultConfiguration() {
TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE);
configuration.setAlarmDetailsBuildMvel(ALARM_DETAILS_BUILD_MVEL_TEMPLATE);
configuration.setAlarmDetailsBuildTbel(ALARM_DETAILS_BUILD_TBEL_TEMPLATE);
configuration.setAlarmType("General Alarm");
return configuration;
}

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java

@ -39,9 +39,9 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
@Override
public TbCreateAlarmNodeConfiguration defaultConfiguration() {
TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE);
configuration.setAlarmDetailsBuildMvel(ALARM_DETAILS_BUILD_MVEL_TEMPLATE);
configuration.setAlarmDetailsBuildTbel(ALARM_DETAILS_BUILD_TBEL_TEMPLATE);
configuration.setAlarmType("General Alarm");
configuration.setSeverity(AlarmSeverity.CRITICAL.name());
configuration.setPropagate(false);

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java

@ -56,7 +56,7 @@ public class TbLogNode implements TbNode {
this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class);
this.standard = new TbLogNodeConfiguration().defaultConfiguration().getJsScript().equals(config.getJsScript());
this.scriptEngine = this.standard ? null : ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getMvelScript() : config.getJsScript());
ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
}
@Override

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNodeConfiguration.java

@ -24,14 +24,14 @@ public class TbLogNodeConfiguration implements NodeConfiguration {
private ScriptLanguage scriptLang;
private String jsScript;
private String mvelScript;
private String tbelScript;
@Override
public TbLogNodeConfiguration defaultConfiguration() {
TbLogNodeConfiguration configuration = new TbLogNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setJsScript("return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);");
configuration.setMvelScript("return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);");
configuration.setTbelScript("return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);");
return configuration;
}
}

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java

@ -96,7 +96,7 @@ public class TbMsgGeneratorNode implements TbNode {
if (ctx.isLocalEntity(originatorId)) {
if (initialized.compareAndSet(false, true)) {
this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getMvelScript() : config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
scheduleTickMsg(ctx);
}
} else if (initialized.compareAndSet(true, false)) {

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java

@ -35,16 +35,16 @@ public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration<TbMsgG
private EntityType originatorType;
private ScriptLanguage scriptLang;
private String jsScript;
private String mvelScript;
private String tbelScript;
@Override
public TbMsgGeneratorNodeConfiguration defaultConfiguration() {
TbMsgGeneratorNodeConfiguration configuration = new TbMsgGeneratorNodeConfiguration();
configuration.setMsgCount(UNLIMITED_MSG_COUNT);
configuration.setPeriodInSeconds(1);
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setJsScript(DEFAULT_SCRIPT);
configuration.setMvelScript(DEFAULT_SCRIPT);
configuration.setTbelScript(DEFAULT_SCRIPT);
return configuration;
}
}

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java

@ -23,7 +23,6 @@ import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import org.thingsboard.server.common.msg.TbMsg;
@ -53,7 +52,7 @@ public class TbJsFilterNode implements TbNode {
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getMvelScript() : config.getJsScript());
ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
}
@Override

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java

@ -24,14 +24,14 @@ public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilter
private ScriptLanguage scriptLang;
private String jsScript;
private String mvelScript;
private String tbelScript;
@Override
public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setJsScript("return msg.temperature > 20;");
configuration.setMvelScript("return msg.temperature > 20;");
configuration.setTbelScript("return msg.temperature > 20;");
return configuration;
}
}

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java

@ -56,7 +56,7 @@ public class TbJsSwitchNode implements TbNode {
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.MVEL.equals(config.getScriptLang()) ? config.getMvelScript() : config.getJsScript());
ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
}
@Override

11
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNodeConfiguration.java

@ -15,13 +15,10 @@
*/
package org.thingsboard.rule.engine.filter;
import com.google.common.collect.Sets;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.script.ScriptLanguage;
import java.util.Set;
@Data
public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitchNodeConfiguration> {
@ -33,7 +30,7 @@ public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitch
"}\n" +
"return nextRelation(metadata, msg);";
private static final String DEFAULT_MVEL_SCRIPT = "function nextRelation(metadata, msg) {\n" +
private static final String DEFAULT_TBEL_SCRIPT = "function nextRelation(metadata, msg) {\n" +
" return ['one','nine'];\n" +
"}\n" +
"if(msgType == 'POST_TELEMETRY_REQUEST') {\n" +
@ -43,14 +40,14 @@ public class TbJsSwitchNodeConfiguration implements NodeConfiguration<TbJsSwitch
private ScriptLanguage scriptLang;
private String jsScript;
private String mvelScript;
private String tbelScript;
@Override
public TbJsSwitchNodeConfiguration defaultConfiguration() {
TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
configuration.setScriptLang(ScriptLanguage.MVEL);
configuration.setScriptLang(ScriptLanguage.TBEL);
configuration.setJsScript(DEFAULT_JS_SCRIPT);
configuration.setMvelScript(DEFAULT_MVEL_SCRIPT);
configuration.setTbelScript(DEFAULT_TBEL_SCRIPT);
return configuration;
}
}

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

Loading…
Cancel
Save