154 changed files with 1739 additions and 775 deletions
@ -0,0 +1,27 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.edge.rpc.utils; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.server.gen.edge.v1.EdgeVersion; |
||||
|
|
||||
|
@Slf4j |
||||
|
public final class EdgeVersionUtils { |
||||
|
|
||||
|
public static boolean isEdgeVersionOlderThan(EdgeVersion currentVersion, EdgeVersion requiredVersion) { |
||||
|
return currentVersion.ordinal() < requiredVersion.ordinal(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.utils; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.rule.engine.api.TbNode; |
||||
|
import org.thingsboard.server.common.data.rule.RuleNode; |
||||
|
import org.thingsboard.server.common.data.util.TbPair; |
||||
|
import org.thingsboard.server.service.component.RuleNodeClassInfo; |
||||
|
|
||||
|
public class TbNodeUpgradeUtils { |
||||
|
|
||||
|
public static void upgradeConfigurationAndVersion(RuleNode node, RuleNodeClassInfo nodeInfo) throws Exception { |
||||
|
JsonNode oldConfiguration = node.getConfiguration(); |
||||
|
if (oldConfiguration == null || !oldConfiguration.isObject()) { |
||||
|
var configClass = nodeInfo.getAnnotation().configClazz(); |
||||
|
node.setConfiguration(JacksonUtil.valueToTree(configClass.getDeclaredConstructor().newInstance().defaultConfiguration())); |
||||
|
} else { |
||||
|
var tbVersionedNode = (TbNode) nodeInfo.getClazz().getDeclaredConstructor().newInstance(); |
||||
|
TbPair<Boolean, JsonNode> upgradeResult = tbVersionedNode.upgrade(node.getConfigurationVersion(), oldConfiguration); |
||||
|
if (upgradeResult.getFirst()) { |
||||
|
node.setConfiguration(upgradeResult.getSecond()); |
||||
|
} |
||||
|
} |
||||
|
node.setConfigurationVersion(nodeInfo.getCurrentVersion()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,159 +0,0 @@ |
|||||
/** |
|
||||
* Copyright © 2016-2023 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.controller; |
|
||||
|
|
||||
import com.fasterxml.jackson.core.type.TypeReference; |
|
||||
import com.fasterxml.jackson.databind.JsonNode; |
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|
||||
import com.google.common.util.concurrent.ListeningExecutorService; |
|
||||
import com.google.common.util.concurrent.MoreExecutors; |
|
||||
import org.junit.After; |
|
||||
import org.junit.Assert; |
|
||||
import org.junit.Before; |
|
||||
import org.junit.Test; |
|
||||
import org.mockito.AdditionalAnswers; |
|
||||
import org.mockito.Mockito; |
|
||||
import org.springframework.context.annotation.Bean; |
|
||||
import org.springframework.context.annotation.Primary; |
|
||||
import org.springframework.test.context.ContextConfiguration; |
|
||||
import org.thingsboard.common.util.JacksonUtil; |
|
||||
import org.thingsboard.common.util.ThingsBoardExecutors; |
|
||||
import org.thingsboard.server.common.data.AdminSettings; |
|
||||
import org.thingsboard.server.common.data.Device; |
|
||||
import org.thingsboard.server.common.data.Tenant; |
|
||||
import org.thingsboard.server.common.data.User; |
|
||||
import org.thingsboard.server.common.data.security.Authority; |
|
||||
import org.thingsboard.server.common.data.security.DeviceCredentials; |
|
||||
import org.thingsboard.server.dao.device.DeviceDao; |
|
||||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|
||||
|
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|
||||
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP; |
|
||||
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS; |
|
||||
|
|
||||
@ContextConfiguration(classes = {DeviceConnectivityControllerWithDefaultPortTest.Config.class}) |
|
||||
@DaoSqlTest |
|
||||
public class DeviceConnectivityControllerWithDefaultPortTest extends AbstractControllerTest { |
|
||||
|
|
||||
ListeningExecutorService executor; |
|
||||
|
|
||||
private Tenant savedTenant; |
|
||||
|
|
||||
static class Config { |
|
||||
@Bean |
|
||||
@Primary |
|
||||
public DeviceDao deviceDao(DeviceDao deviceDao) { |
|
||||
return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Before |
|
||||
public void beforeTest() throws Exception { |
|
||||
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); |
|
||||
|
|
||||
loginSysAdmin(); |
|
||||
|
|
||||
ObjectNode config = JacksonUtil.newObjectNode(); |
|
||||
|
|
||||
ObjectNode http = JacksonUtil.newObjectNode(); |
|
||||
http.put("enabled", true); |
|
||||
http.put("host", ""); |
|
||||
http.put("port", 80); |
|
||||
config.set("http", http); |
|
||||
|
|
||||
ObjectNode https = JacksonUtil.newObjectNode(); |
|
||||
https.put("enabled", true); |
|
||||
https.put("host", ""); |
|
||||
https.put("port", 443); |
|
||||
config.set("https", https); |
|
||||
|
|
||||
ObjectNode mqtt = JacksonUtil.newObjectNode(); |
|
||||
mqtt.put("enabled", false); |
|
||||
mqtt.put("host", ""); |
|
||||
mqtt.put("port", 1883); |
|
||||
config.set("mqtt", mqtt); |
|
||||
|
|
||||
ObjectNode mqtts = JacksonUtil.newObjectNode(); |
|
||||
mqtts.put("enabled", false); |
|
||||
mqtts.put("host", ""); |
|
||||
mqtts.put("port", 8883); |
|
||||
config.set("mqtts", mqtts); |
|
||||
|
|
||||
ObjectNode coap = JacksonUtil.newObjectNode(); |
|
||||
coap.put("enabled", false); |
|
||||
coap.put("host", ""); |
|
||||
coap.put("port", 5683); |
|
||||
config.set("coap", coap); |
|
||||
|
|
||||
ObjectNode coaps = JacksonUtil.newObjectNode(); |
|
||||
coaps.put("enabled", false); |
|
||||
coaps.put("host", ""); |
|
||||
coaps.put("port", 5684); |
|
||||
config.set("coaps", coaps); |
|
||||
|
|
||||
AdminSettings adminSettings = doGet("/api/admin/settings/connectivity", AdminSettings.class); |
|
||||
adminSettings.setJsonValue(config); |
|
||||
doPost("/api/admin/settings", adminSettings).andExpect(status().isOk()); |
|
||||
|
|
||||
Tenant tenant = new Tenant(); |
|
||||
tenant.setTitle("My tenant"); |
|
||||
savedTenant = doPost("/api/tenant", tenant, Tenant.class); |
|
||||
Assert.assertNotNull(savedTenant); |
|
||||
|
|
||||
User tenantAdmin = new User(); |
|
||||
tenantAdmin.setAuthority(Authority.TENANT_ADMIN); |
|
||||
tenantAdmin.setTenantId(savedTenant.getId()); |
|
||||
tenantAdmin.setEmail("tenant2@thingsboard.org"); |
|
||||
tenantAdmin.setFirstName("Joe"); |
|
||||
tenantAdmin.setLastName("Downs"); |
|
||||
|
|
||||
createUserAndLogin(tenantAdmin, "testPassword1"); |
|
||||
} |
|
||||
|
|
||||
@After |
|
||||
public void afterTest() throws Exception { |
|
||||
executor.shutdownNow(); |
|
||||
|
|
||||
loginSysAdmin(); |
|
||||
|
|
||||
doDelete("/api/tenant/" + savedTenant.getId().getId()) |
|
||||
.andExpect(status().isOk()); |
|
||||
} |
|
||||
|
|
||||
@Test |
|
||||
public void testFetchPublishTelemetryCommandsForDefaultDevice() throws Exception { |
|
||||
Device device = new Device(); |
|
||||
device.setName("My device"); |
|
||||
device.setType("default"); |
|
||||
Device savedDevice = doPost("/api/device", device, Device.class); |
|
||||
JsonNode commands = |
|
||||
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() { |
|
||||
}); |
|
||||
|
|
||||
DeviceCredentials credentials = |
|
||||
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class); |
|
||||
|
|
||||
assertThat(commands).hasSize(1); |
|
||||
JsonNode httpCommands = commands.get(HTTP); |
|
||||
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost/api/v1/%s/telemetry " + |
|
||||
"--header Content-Type:application/json --data \"{temperature:25}\"", |
|
||||
credentials.getCredentialsId())); |
|
||||
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost/api/v1/%s/telemetry " + |
|
||||
"--header Content-Type:application/json --data \"{temperature:25}\"", |
|
||||
credentials.getCredentialsId())); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,42 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.edge.rpc.processor.telemetry; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.node.ObjectNode; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.junit.Assert; |
||||
|
import org.junit.Test; |
||||
|
import org.junit.runner.RunWith; |
||||
|
import org.mockito.junit.MockitoJUnitRunner; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.StringUtils; |
||||
|
import org.thingsboard.server.common.data.edge.EdgeEvent; |
||||
|
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; |
||||
|
|
||||
|
@Slf4j |
||||
|
@RunWith(MockitoJUnitRunner.class) |
||||
|
public class TelemetryEdgeProcessorTest { |
||||
|
|
||||
|
@Test |
||||
|
public void testConvert_maxSizeLimit() throws Exception { |
||||
|
EdgeEvent edgeEvent = new EdgeEvent(); |
||||
|
ObjectNode body = JacksonUtil.newObjectNode(); |
||||
|
body.put("value", StringUtils.randomAlphanumeric(10000)); |
||||
|
edgeEvent.setBody(body); |
||||
|
DownlinkMsg downlinkMsg = new TelemetryEdgeProcessor().convertTelemetryEventToDownlink(edgeEvent); |
||||
|
Assert.assertNull(downlinkMsg); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,309 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.transport.coap.client; |
||||
|
|
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import lombok.Getter; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.eclipse.californium.core.CoapHandler; |
||||
|
import org.eclipse.californium.core.CoapObserveRelation; |
||||
|
import org.eclipse.californium.core.CoapResponse; |
||||
|
import org.eclipse.californium.core.coap.CoAP; |
||||
|
import org.eclipse.californium.core.coap.MediaTypeRegistry; |
||||
|
import org.junit.After; |
||||
|
import org.junit.Before; |
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.query.EntityKey; |
||||
|
import org.thingsboard.server.common.data.query.EntityKeyType; |
||||
|
import org.thingsboard.server.common.data.query.SingleEntityFilter; |
||||
|
import org.thingsboard.server.common.msg.session.FeatureType; |
||||
|
import org.thingsboard.server.dao.service.DaoSqlTest; |
||||
|
import org.thingsboard.server.transport.coap.AbstractCoapIntegrationTest; |
||||
|
import org.thingsboard.server.transport.coap.CoapTestCallback; |
||||
|
import org.thingsboard.server.transport.coap.CoapTestClient; |
||||
|
import org.thingsboard.server.transport.coap.CoapTestConfigProperties; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.LinkedHashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.awaitility.Awaitility.await; |
||||
|
import static org.junit.Assert.assertArrayEquals; |
||||
|
import static org.junit.Assert.assertEquals; |
||||
|
import static org.junit.Assert.assertNotNull; |
||||
|
import static org.junit.Assert.assertTrue; |
||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
||||
|
import static org.thingsboard.server.common.data.query.EntityKeyType.CLIENT_ATTRIBUTE; |
||||
|
import static org.thingsboard.server.common.data.query.EntityKeyType.SHARED_ATTRIBUTE; |
||||
|
|
||||
|
@Slf4j |
||||
|
@DaoSqlTest |
||||
|
public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { |
||||
|
|
||||
|
private static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," + |
||||
|
" \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}"; |
||||
|
private static final List<String> EXPECTED_KEYS = Arrays.asList("key1", "key2", "key3", "key4", "key5"); |
||||
|
private static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}"; |
||||
|
|
||||
|
|
||||
|
@Before |
||||
|
public void beforeTest() throws Exception { |
||||
|
CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder() |
||||
|
.deviceName("Test Post Attributes device") |
||||
|
.build(); |
||||
|
processBeforeTest(configProperties); |
||||
|
} |
||||
|
|
||||
|
@After |
||||
|
public void afterTest() throws Exception { |
||||
|
processAfterTest(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testConfirmableRequests() throws Exception { |
||||
|
boolean confirmable = true; |
||||
|
processAttributesTest(confirmable); |
||||
|
processTwoWayRpcTest(confirmable); |
||||
|
processTestRequestAttributesValuesFromTheServer(confirmable); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testNonConfirmableRequests() throws Exception { |
||||
|
boolean confirmable = false; |
||||
|
processAttributesTest(confirmable); |
||||
|
processTwoWayRpcTest(confirmable); |
||||
|
processTestRequestAttributesValuesFromTheServer(confirmable); |
||||
|
} |
||||
|
|
||||
|
protected void processAttributesTest(boolean confirmable) throws Exception { |
||||
|
client = createClientForFeatureWithConfirmableParameter(FeatureType.ATTRIBUTES, confirmable); |
||||
|
CoapResponse coapResponse = client.postMethod(PAYLOAD_VALUES_STR.getBytes()); |
||||
|
assertEquals(CoAP.ResponseCode.CREATED, coapResponse.getCode()); |
||||
|
assertEquals("CoAP response type is wrong!", client.getType(), coapResponse.advanced().getType()); |
||||
|
|
||||
|
DeviceId deviceId = savedDevice.getId(); |
||||
|
List<String> actualKeys = getActualKeysList(deviceId); |
||||
|
assertNotNull(actualKeys); |
||||
|
|
||||
|
Set<String> actualKeySet = new HashSet<>(actualKeys); |
||||
|
Set<String> expectedKeySet = new HashSet<>(EXPECTED_KEYS); |
||||
|
assertEquals(expectedKeySet, actualKeySet); |
||||
|
|
||||
|
String attributesValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/attributes/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); |
||||
|
; |
||||
|
List<Map<String, Object>> values = doGetAsyncTyped(attributesValuesUrl, new TypeReference<>() { |
||||
|
}); |
||||
|
assertAttributesValues(values, actualKeySet); |
||||
|
String deleteAttributesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); |
||||
|
doDelete(deleteAttributesUrl); |
||||
|
} |
||||
|
|
||||
|
protected void processTwoWayRpcTest(boolean confirmable) throws Exception { |
||||
|
client = createClientForFeatureWithConfirmableParameter(FeatureType.RPC, confirmable); |
||||
|
CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client); |
||||
|
|
||||
|
CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap, confirmable); |
||||
|
String awaitAlias = "await Two Way Rpc (client.getObserveRelation)"; |
||||
|
await(awaitAlias) |
||||
|
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) |
||||
|
.until(() -> CoAP.ResponseCode.VALID.equals(callbackCoap.getResponseCode()) && |
||||
|
callbackCoap.getObserve() != null && |
||||
|
0 == callbackCoap.getObserve()); |
||||
|
validateCurrentStateNotification(callbackCoap); |
||||
|
|
||||
|
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; |
||||
|
String deviceId = savedDevice.getId().getId().toString(); |
||||
|
int expectedObserveCountAfterGpioRequest1 = callbackCoap.getObserve() + 1; |
||||
|
String actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); |
||||
|
awaitAlias = "await Two Way Rpc (setGpio(method, params, value) first"; |
||||
|
await(awaitAlias) |
||||
|
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) |
||||
|
.until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && |
||||
|
callbackCoap.getObserve() != null && |
||||
|
expectedObserveCountAfterGpioRequest1 == callbackCoap.getObserve()); |
||||
|
validateTwoWayStateChangedNotification(callbackCoap, actualResult); |
||||
|
|
||||
|
int expectedObserveCountAfterGpioRequest2 = callbackCoap.getObserve() + 1; |
||||
|
actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); |
||||
|
awaitAlias = "await Two Way Rpc (setGpio(method, params, value) second"; |
||||
|
await(awaitAlias) |
||||
|
.atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) |
||||
|
.until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && |
||||
|
callbackCoap.getObserve() != null && |
||||
|
expectedObserveCountAfterGpioRequest2 == callbackCoap.getObserve()); |
||||
|
|
||||
|
validateTwoWayStateChangedNotification(callbackCoap, actualResult); |
||||
|
|
||||
|
observeRelation.proactiveCancel(); |
||||
|
assertTrue(observeRelation.isCanceled()); |
||||
|
} |
||||
|
|
||||
|
protected void processTestRequestAttributesValuesFromTheServer(boolean confirmable) throws Exception { |
||||
|
client = createClientForFeatureWithConfirmableParameter(FeatureType.ATTRIBUTES, confirmable); |
||||
|
SingleEntityFilter dtf = new SingleEntityFilter(); |
||||
|
dtf.setSingleEntity(savedDevice.getId()); |
||||
|
List<EntityKey> csKeys = getEntityKeys(CLIENT_ATTRIBUTE); |
||||
|
List<EntityKey> shKeys = getEntityKeys(SHARED_ATTRIBUTE); |
||||
|
List<EntityKey> keys = new ArrayList<>(); |
||||
|
keys.addAll(csKeys); |
||||
|
keys.addAll(shKeys); |
||||
|
getWsClient().subscribeLatestUpdate(keys, dtf); |
||||
|
getWsClient().registerWaitForUpdate(2); |
||||
|
|
||||
|
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", |
||||
|
PAYLOAD_VALUES_STR, String.class, status().isOk()); |
||||
|
|
||||
|
CoapResponse coapResponse = client.postMethod(PAYLOAD_VALUES_STR); |
||||
|
assertEquals(CoAP.ResponseCode.CREATED, coapResponse.getCode()); |
||||
|
|
||||
|
String update = getWsClient().waitForUpdate(); |
||||
|
assertThat(update).as("ws update received").isNotBlank(); |
||||
|
|
||||
|
String keysParam = String.join(",", EXPECTED_KEYS); |
||||
|
String featureTokenUrl = CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.ATTRIBUTES) + "?clientKeys=" + keysParam + "&sharedKeys=" + keysParam; |
||||
|
client.setURI(featureTokenUrl); |
||||
|
CoapResponse response = client.getMethod(); |
||||
|
assertEquals("CoAP response type is wrong!", client.getType(), response.advanced().getType()); |
||||
|
} |
||||
|
|
||||
|
@SuppressWarnings({"unchecked", "rawtypes"}) |
||||
|
protected void assertAttributesValues(List<Map<String, Object>> deviceValues, Set<String> keySet) { |
||||
|
for (Map<String, Object> map : deviceValues) { |
||||
|
String key = (String) map.get("key"); |
||||
|
Object value = map.get("value"); |
||||
|
assertTrue(keySet.contains(key)); |
||||
|
switch (key) { |
||||
|
case "key1": |
||||
|
assertEquals("value1", value); |
||||
|
break; |
||||
|
case "key2": |
||||
|
assertEquals(true, value); |
||||
|
break; |
||||
|
case "key3": |
||||
|
assertEquals(3.0, value); |
||||
|
break; |
||||
|
case "key4": |
||||
|
assertEquals(4, value); |
||||
|
break; |
||||
|
case "key5": |
||||
|
assertNotNull(value); |
||||
|
assertEquals(3, ((LinkedHashMap) value).size()); |
||||
|
assertEquals(42, ((LinkedHashMap) value).get("someNumber")); |
||||
|
assertEquals(Arrays.asList(1, 2, 3), ((LinkedHashMap) value).get("someArray")); |
||||
|
LinkedHashMap<String, String> someNestedObject = (LinkedHashMap) ((LinkedHashMap) value).get("someNestedObject"); |
||||
|
assertEquals("value", someNestedObject.get("key")); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private List<String> getActualKeysList(DeviceId deviceId) throws Exception { |
||||
|
long start = System.currentTimeMillis(); |
||||
|
long end = System.currentTimeMillis() + 5000; |
||||
|
|
||||
|
List<String> actualKeys = null; |
||||
|
while (start <= end) { |
||||
|
actualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() { |
||||
|
}); |
||||
|
if (actualKeys.size() == EXPECTED_KEYS.size()) { |
||||
|
break; |
||||
|
} |
||||
|
Thread.sleep(100); |
||||
|
start += 100; |
||||
|
} |
||||
|
return actualKeys; |
||||
|
} |
||||
|
|
||||
|
private void validateCurrentStateNotification(CoapTestCallback callback) { |
||||
|
assertArrayEquals(EMPTY_PAYLOAD, callback.getPayloadBytes()); |
||||
|
} |
||||
|
|
||||
|
private void validateTwoWayStateChangedNotification(CoapTestCallback callback, String actualResult) { |
||||
|
assertEquals(DEVICE_RESPONSE, actualResult); |
||||
|
assertNotNull(callback.getPayloadBytes()); |
||||
|
} |
||||
|
|
||||
|
protected class TestCoapCallbackForRPC extends CoapTestCallback { |
||||
|
|
||||
|
private final CoapTestClient client; |
||||
|
|
||||
|
@Getter |
||||
|
private boolean wasSuccessful = false; |
||||
|
|
||||
|
TestCoapCallbackForRPC(CoapTestClient client) { |
||||
|
this.client = client; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onLoad(CoapResponse response) { |
||||
|
payloadBytes = response.getPayload(); |
||||
|
responseCode = response.getCode(); |
||||
|
observe = response.getOptions().getObserve(); |
||||
|
wasSuccessful = client.getType().equals(response.advanced().getType()); |
||||
|
if (observe != null) { |
||||
|
if (observe > 0) { |
||||
|
processOnLoadResponse(response, client); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onError() { |
||||
|
log.warn("Command Response Ack Error, No connect"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected void processOnLoadResponse(CoapResponse response, CoapTestClient client) { |
||||
|
JsonNode responseJson = JacksonUtil.fromBytes(response.getPayload()); |
||||
|
int requestId = responseJson.get("id").asInt(); |
||||
|
client.setURI(CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.RPC, requestId)); |
||||
|
client.postMethod(new CoapHandler() { |
||||
|
@Override |
||||
|
public void onLoad(CoapResponse response) { |
||||
|
log.warn("RPC {} command response ack: {}", requestId, response.getCode()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onError() { |
||||
|
log.warn("RPC {} command response ack error, no connect", requestId); |
||||
|
} |
||||
|
}, DEVICE_RESPONSE, MediaTypeRegistry.APPLICATION_JSON); |
||||
|
} |
||||
|
|
||||
|
private CoapTestClient createClientForFeatureWithConfirmableParameter(FeatureType featureType, boolean confirmable) { |
||||
|
CoapTestClient coapTestClient = new CoapTestClient(accessToken, featureType); |
||||
|
if (confirmable) { |
||||
|
coapTestClient.useCONs(); |
||||
|
} else { |
||||
|
coapTestClient.useNONs(); |
||||
|
} |
||||
|
return coapTestClient; |
||||
|
} |
||||
|
|
||||
|
private List<EntityKey> getEntityKeys(EntityKeyType scope) { |
||||
|
return CoapClientIntegrationTest.EXPECTED_KEYS.stream().map(key -> new EntityKey(scope, key)).collect(Collectors.toList()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,105 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.utils; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.node.NullNode; |
||||
|
import org.assertj.core.api.Assertions; |
||||
|
import org.junit.Test; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode; |
||||
|
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; |
||||
|
import org.thingsboard.server.common.data.rule.RuleNode; |
||||
|
import org.thingsboard.server.service.component.RuleNodeClassInfo; |
||||
|
|
||||
|
import static org.mockito.Mockito.mock; |
||||
|
import static org.mockito.Mockito.when; |
||||
|
|
||||
|
public class TbNodeUpgradeUtilsTest { |
||||
|
|
||||
|
@Test |
||||
|
public void testUpgradeRuleNodeConfigurationWithNullConfig() throws Exception { |
||||
|
// GIVEN
|
||||
|
var node = new RuleNode(); |
||||
|
var nodeInfo = mock(RuleNodeClassInfo.class); |
||||
|
var nodeConfigClazz = TbGetAttributesNodeConfiguration.class; |
||||
|
var annotation = mock(org.thingsboard.rule.engine.api.RuleNode.class); |
||||
|
var defaultConfig = JacksonUtil.valueToTree(nodeConfigClazz.getDeclaredConstructor().newInstance().defaultConfiguration()); |
||||
|
|
||||
|
when(nodeInfo.getClazz()).thenReturn((Class)TbGetAttributesNode.class); |
||||
|
when(nodeInfo.getCurrentVersion()).thenReturn(1); |
||||
|
when(nodeInfo.getAnnotation()).thenReturn(annotation); |
||||
|
when(annotation.configClazz()).thenReturn((Class) nodeConfigClazz); |
||||
|
|
||||
|
// WHEN
|
||||
|
TbNodeUpgradeUtils.upgradeConfigurationAndVersion(node, nodeInfo); |
||||
|
// THEN
|
||||
|
Assertions.assertThat(node.getConfiguration()).isEqualTo(defaultConfig); |
||||
|
Assertions.assertThat(node.getConfigurationVersion()).isEqualTo(1); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUpgradeRuleNodeConfigurationWithNullNodeConfig() throws Exception { |
||||
|
// GIVEN
|
||||
|
var node = new RuleNode(); |
||||
|
node.setConfiguration(NullNode.instance); |
||||
|
var nodeInfo = mock(RuleNodeClassInfo.class); |
||||
|
var nodeConfigClazz = TbGetAttributesNodeConfiguration.class; |
||||
|
var annotation = mock(org.thingsboard.rule.engine.api.RuleNode.class); |
||||
|
var defaultConfig = JacksonUtil.valueToTree(nodeConfigClazz.getDeclaredConstructor().newInstance().defaultConfiguration()); |
||||
|
|
||||
|
when(nodeInfo.getClazz()).thenReturn((Class)TbGetAttributesNode.class); |
||||
|
when(nodeInfo.getCurrentVersion()).thenReturn(1); |
||||
|
when(nodeInfo.getAnnotation()).thenReturn(annotation); |
||||
|
when(annotation.configClazz()).thenReturn((Class) nodeConfigClazz); |
||||
|
|
||||
|
// WHEN
|
||||
|
TbNodeUpgradeUtils.upgradeConfigurationAndVersion(node, nodeInfo); |
||||
|
// THEN
|
||||
|
Assertions.assertThat(node.getConfiguration()).isEqualTo(defaultConfig); |
||||
|
Assertions.assertThat(node.getConfigurationVersion()).isEqualTo(1); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void testUpgradeRuleNodeConfigurationWithNonNullConfig() throws Exception { |
||||
|
// GIVEN
|
||||
|
var node = new RuleNode(); |
||||
|
var nodeInfo = mock(RuleNodeClassInfo.class); |
||||
|
var nodeConfigClazz = TbGetAttributesNodeConfiguration.class; |
||||
|
var annotation = mock(org.thingsboard.rule.engine.api.RuleNode.class); |
||||
|
var defaultConfig = JacksonUtil.valueToTree(nodeConfigClazz.getDeclaredConstructor().newInstance().defaultConfiguration()); |
||||
|
|
||||
|
when(nodeInfo.getClazz()).thenReturn((Class)TbGetAttributesNode.class); |
||||
|
when(nodeInfo.getCurrentVersion()).thenReturn(1); |
||||
|
when(nodeInfo.getAnnotation()).thenReturn(annotation); |
||||
|
when(annotation.configClazz()).thenReturn((Class) nodeConfigClazz); |
||||
|
|
||||
|
String versionZeroDefaultConfigStr = "{\"fetchToData\":false," + |
||||
|
"\"clientAttributeNames\":[]," + |
||||
|
"\"sharedAttributeNames\":[]," + |
||||
|
"\"serverAttributeNames\":[]," + |
||||
|
"\"latestTsKeyNames\":[]," + |
||||
|
"\"tellFailureIfAbsent\":true," + |
||||
|
"\"getLatestValueWithTs\":false}"; |
||||
|
node.setConfiguration(JacksonUtil.toJsonNode(versionZeroDefaultConfigStr)); |
||||
|
// WHEN
|
||||
|
TbNodeUpgradeUtils.upgradeConfigurationAndVersion(node, nodeInfo); |
||||
|
// THEN
|
||||
|
Assertions.assertThat(node.getConfiguration()).isEqualTo(defaultConfig); |
||||
|
Assertions.assertThat(node.getConfigurationVersion()).isEqualTo(1); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.common.transport.limits; |
||||
|
|
||||
|
import com.github.benmanes.caffeine.cache.Cache; |
||||
|
import com.github.benmanes.caffeine.cache.Caffeine; |
||||
|
import com.github.benmanes.caffeine.cache.Expiry; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.jetbrains.annotations.NotNull; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.server.queue.util.TbTransportComponent; |
||||
|
|
||||
|
import java.util.concurrent.ThreadLocalRandom; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
@Service |
||||
|
@TbTransportComponent |
||||
|
@Slf4j |
||||
|
public class DefaultEntityLimitsCache implements EntityLimitsCache { |
||||
|
|
||||
|
private static final int DEVIATION = 10; |
||||
|
private final Cache<EntityLimitKey, Boolean> cache; |
||||
|
|
||||
|
public DefaultEntityLimitsCache(@Value("${cache.entityLimits.timeToLiveInMinutes:5}") int ttl, |
||||
|
@Value("${cache.entityLimits.maxSize:100000}") int maxSize) { |
||||
|
// We use the 'random' expiration time to avoid peak loads.
|
||||
|
long mainPart = (TimeUnit.MINUTES.toNanos(ttl) / 100) * (100 - DEVIATION); |
||||
|
long randomPart = (TimeUnit.MINUTES.toNanos(ttl) / 100) * DEVIATION; |
||||
|
cache = Caffeine.newBuilder() |
||||
|
.expireAfter(new Expiry<EntityLimitKey, Boolean>() { |
||||
|
@Override |
||||
|
public long expireAfterCreate(@NotNull EntityLimitKey key, @NotNull Boolean value, long currentTime) { |
||||
|
return mainPart + (long) (randomPart * ThreadLocalRandom.current().nextDouble()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public long expireAfterUpdate(@NotNull EntityLimitKey key, @NotNull Boolean value, long currentTime, long currentDuration) { |
||||
|
return currentDuration; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public long expireAfterRead(@NotNull EntityLimitKey key, @NotNull Boolean value, long currentTime, long currentDuration) { |
||||
|
return currentDuration; |
||||
|
} |
||||
|
}) |
||||
|
.maximumSize(maxSize) |
||||
|
.build(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean get(EntityLimitKey key) { |
||||
|
var result = cache.getIfPresent(key); |
||||
|
return result != null ? result : false; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void put(EntityLimitKey key, boolean value) { |
||||
|
cache.put(key, value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.common.transport.limits; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
@Data |
||||
|
public class EntityLimitKey { |
||||
|
|
||||
|
private final TenantId tenantId; |
||||
|
private final String deviceName; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.common.transport.limits; |
||||
|
|
||||
|
public interface EntityLimitsCache { |
||||
|
|
||||
|
boolean get(EntityLimitKey key); |
||||
|
|
||||
|
void put(EntityLimitKey key, boolean value); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2023 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.exception; |
||||
|
|
||||
|
import lombok.Getter; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
public class EntitiesLimitException extends DataValidationException { |
||||
|
private static final long serialVersionUID = -9211462514373279196L; |
||||
|
|
||||
|
@Getter |
||||
|
private final TenantId tenantId; |
||||
|
@Getter |
||||
|
private final EntityType entityType; |
||||
|
|
||||
|
public EntitiesLimitException(TenantId tenantId, EntityType entityType) { |
||||
|
super(entityType.getNormalName() + "s limit reached"); |
||||
|
this.tenantId = tenantId; |
||||
|
this.entityType = entityType; |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue