committed by
GitHub
105 changed files with 1952 additions and 1125 deletions
@ -0,0 +1,65 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.edge.instructions; |
||||
|
|
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.Setter; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.thingsboard.server.service.install.InstallScripts; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.nio.file.Files; |
||||
|
import java.nio.file.Path; |
||||
|
import java.nio.file.Paths; |
||||
|
|
||||
|
@Slf4j |
||||
|
@RequiredArgsConstructor |
||||
|
public abstract class BaseEdgeInstallUpgradeInstructionsService { |
||||
|
|
||||
|
private static final String EDGE_DIR = "edge"; |
||||
|
private static final String INSTRUCTIONS_DIR = "instructions"; |
||||
|
|
||||
|
private final InstallScripts installScripts; |
||||
|
|
||||
|
@Value("${app.version:unknown}") |
||||
|
@Setter |
||||
|
protected String appVersion; |
||||
|
|
||||
|
protected String readFile(Path file) { |
||||
|
try { |
||||
|
return Files.readString(file); |
||||
|
} catch (IOException e) { |
||||
|
log.warn("Failed to read file: {}", file, e); |
||||
|
throw new RuntimeException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected String getTagVersion(String version) { |
||||
|
return version.endsWith(".0") ? version.substring(0, version.length() - 2) : version; |
||||
|
} |
||||
|
|
||||
|
protected Path resolveFile(String subDir, String... subDirs) { |
||||
|
return getEdgeInstructionsDir().resolve(Paths.get(subDir, subDirs)); |
||||
|
} |
||||
|
|
||||
|
protected Path getEdgeInstructionsDir() { |
||||
|
return Paths.get(installScripts.getDataDir(), InstallScripts.JSON_DIR, EDGE_DIR, INSTRUCTIONS_DIR, getBaseDirName()); |
||||
|
} |
||||
|
|
||||
|
protected abstract String getBaseDirName(); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,421 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.rule.engine.rpc; |
||||
|
|
||||
|
import org.junit.jupiter.api.BeforeEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
import org.junit.jupiter.params.ParameterizedTest; |
||||
|
import org.junit.jupiter.params.provider.Arguments; |
||||
|
import org.junit.jupiter.params.provider.EnumSource; |
||||
|
import org.junit.jupiter.params.provider.MethodSource; |
||||
|
import org.junit.jupiter.params.provider.NullAndEmptySource; |
||||
|
import org.junit.jupiter.params.provider.ValueSource; |
||||
|
import org.mockito.ArgumentCaptor; |
||||
|
import org.mockito.Mock; |
||||
|
import org.mockito.junit.jupiter.MockitoExtension; |
||||
|
import org.springframework.test.util.ReflectionTestUtils; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; |
||||
|
import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcResponse; |
||||
|
import org.thingsboard.rule.engine.api.RuleEngineRpcService; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.DataConstants; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityIdFactory; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.msg.TbMsgType; |
||||
|
import org.thingsboard.server.common.data.msg.TbNodeConnectionType; |
||||
|
import org.thingsboard.server.common.data.rpc.RpcError; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
|
||||
|
import java.util.Optional; |
||||
|
import java.util.Random; |
||||
|
import java.util.UUID; |
||||
|
import java.util.function.Consumer; |
||||
|
import java.util.stream.Stream; |
||||
|
|
||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
import static org.mockito.ArgumentMatchers.any; |
||||
|
import static org.mockito.ArgumentMatchers.eq; |
||||
|
import static org.mockito.BDDMockito.given; |
||||
|
import static org.mockito.BDDMockito.then; |
||||
|
import static org.mockito.BDDMockito.willAnswer; |
||||
|
import static org.mockito.Mockito.mock; |
||||
|
|
||||
|
@ExtendWith(MockitoExtension.class) |
||||
|
public class TbSendRPCRequestNodeTest { |
||||
|
|
||||
|
private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("d3a47f8b-d863-4c1f-b6f0-2c946b43f21c")); |
||||
|
private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("b052ae59-b9b4-47e8-ac71-39e7124bbd66")); |
||||
|
|
||||
|
private final String MSG_DATA = """ |
||||
|
{ |
||||
|
"method": "setGpio", |
||||
|
"params": { |
||||
|
"pin": "23", |
||||
|
"value": 1 |
||||
|
}, |
||||
|
"additionalInfo": "information" |
||||
|
} |
||||
|
"""; |
||||
|
|
||||
|
private TbSendRPCRequestNode node; |
||||
|
private TbSendRpcRequestNodeConfiguration config; |
||||
|
|
||||
|
@Mock |
||||
|
private TbContext ctxMock; |
||||
|
@Mock |
||||
|
private RuleEngineRpcService rpcServiceMock; |
||||
|
|
||||
|
@BeforeEach |
||||
|
public void setUp() throws TbNodeException { |
||||
|
node = new TbSendRPCRequestNode(); |
||||
|
config = new TbSendRpcRequestNodeConfiguration().defaultConfiguration(); |
||||
|
var configuration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); |
||||
|
node.init(ctxMock, configuration); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void verifyDefaultConfig() { |
||||
|
assertThat(config.getTimeoutInSeconds()).isEqualTo(60); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource |
||||
|
public void givenOneway_whenOnMsg_thenVerifyRequest(String mdKeyValue, boolean expectedResult) { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
TbMsgMetaData msgMetadata = new TbMsgMetaData(); |
||||
|
msgMetadata.putValue("oneway", mdKeyValue); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, msgMetadata, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
var ruleEngineDeviceRpcRequestCaptor = captureRequest(); |
||||
|
assertThat(ruleEngineDeviceRpcRequestCaptor.getValue().isOneway()).isEqualTo(expectedResult); |
||||
|
} |
||||
|
|
||||
|
private static Stream<Arguments> givenOneway_whenOnMsg_thenVerifyRequest() { |
||||
|
return Stream.of( |
||||
|
Arguments.of("true", true), |
||||
|
Arguments.of("false", false), |
||||
|
Arguments.of(null, false), |
||||
|
Arguments.of("", false) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenMsgBody_whenOnMsg_thenVerifyRequest() { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = ArgumentCaptor.forClass(RuleEngineDeviceRpcRequest.class); |
||||
|
then(rpcServiceMock).should().sendRpcRequestToDevice(requestCaptor.capture(), any(Consumer.class)); |
||||
|
assertThat(requestCaptor.getValue()) |
||||
|
.hasFieldOrPropertyWithValue("method", "setGpio") |
||||
|
.hasFieldOrPropertyWithValue("body", "{\"pin\":\"23\",\"value\":1}") |
||||
|
.hasFieldOrPropertyWithValue("deviceId", DEVICE_ID) |
||||
|
.hasFieldOrPropertyWithValue("tenantId", TENANT_ID) |
||||
|
.hasFieldOrPropertyWithValue("additionalInfo", "information"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenRequestIdIsNotSet_whenOnMsg_thenVerifyRequest() { |
||||
|
Random randomMock = mock(Random.class); |
||||
|
given(randomMock.nextInt()).willReturn(123); |
||||
|
ReflectionTestUtils.setField(node, "random", randomMock); |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().getRequestId()).isEqualTo(123); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenRequestId_whenOnMsg_thenVerifyRequest() { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
String data = """ |
||||
|
{ |
||||
|
"method": "setGpio", |
||||
|
"params": { |
||||
|
"pin": "23", |
||||
|
"value": 1 |
||||
|
}, |
||||
|
"requestId": 12345 |
||||
|
} |
||||
|
"""; |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.TO_SERVER_RPC_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, data); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().getRequestId()).isEqualTo(12345); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenRequestUUID_whenOnMsg_thenVerifyRequest() { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
String requestUUID = "b795a241-5a30-48fb-92d5-46b864d47130"; |
||||
|
TbMsgMetaData metadata = new TbMsgMetaData(); |
||||
|
metadata.putValue("requestUUID", requestUUID); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().getRequestUUID()).isEqualTo(UUID.fromString(requestUUID)); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@NullAndEmptySource |
||||
|
public void givenInvalidRequestUUID_whenOnMsg_thenVerifyRequest(String requestUUID) { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
TbMsgMetaData metadata = new TbMsgMetaData(); |
||||
|
metadata.putValue("requestUUID", requestUUID); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().getRequestUUID()).isNotNull(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenOriginServiceId_whenOnMsg_thenVerifyRequest() { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
String originServiceId = "service-id-123"; |
||||
|
TbMsgMetaData metadata = new TbMsgMetaData(); |
||||
|
metadata.putValue("originServiceId", originServiceId); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().getOriginServiceId()).isEqualTo(originServiceId); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@NullAndEmptySource |
||||
|
public void givenInvalidOriginServiceId_whenOnMsg_thenVerifyRequest(String originServiceId) { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
TbMsgMetaData metadata = new TbMsgMetaData(); |
||||
|
metadata.putValue("originServiceId", originServiceId); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().getOriginServiceId()).isNull(); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenExpirationTime_whenOnMsg_thenVerifyRequest() { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
String expirationTime = "2000000000000"; |
||||
|
TbMsgMetaData metadata = new TbMsgMetaData(); |
||||
|
metadata.putValue(DataConstants.EXPIRATION_TIME, expirationTime); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().getExpirationTime()).isEqualTo(Long.parseLong(expirationTime)); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@NullAndEmptySource |
||||
|
public void givenInvalidExpirationTime_whenOnMsg_thenVerifyRequest(String expirationTime) { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
TbMsgMetaData metadata = new TbMsgMetaData(); |
||||
|
metadata.putValue(DataConstants.EXPIRATION_TIME, expirationTime); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().getExpirationTime()).isGreaterThan(System.currentTimeMillis()); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenRetries_whenOnMsg_thenVerifyRequest() { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
Integer retries = 3; |
||||
|
TbMsgMetaData metadata = new TbMsgMetaData(); |
||||
|
metadata.putValue(DataConstants.RETRIES, String.valueOf(retries)); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().getRetries()).isEqualTo(retries); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@NullAndEmptySource |
||||
|
public void givenInvalidRetriesValue_whenOnMsg_thenVerifyRequest(String retries) { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
TbMsgMetaData metadata = new TbMsgMetaData(); |
||||
|
metadata.putValue(DataConstants.RETRIES, retries); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().getRetries()).isNull(); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@EnumSource(TbMsgType.class) |
||||
|
public void givenTbMsgType_whenOnMsg_thenVerifyRequest(TbMsgType msgType) { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
TbMsg msg = TbMsg.newMsg(msgType, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
if (msgType == TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE) { |
||||
|
assertThat(requestCaptor.getValue().isRestApiCall()).isTrue(); |
||||
|
return; |
||||
|
} |
||||
|
assertThat(requestCaptor.getValue().isRestApiCall()).isFalse(); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@MethodSource |
||||
|
public void givenPersistent_whenOnMsg_thenVerifyRequest(String isPersisted, boolean expectedPersistence) { |
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
|
||||
|
TbMsgMetaData metadata = new TbMsgMetaData(); |
||||
|
metadata.putValue(DataConstants.PERSISTENT, isPersisted); |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, metadata, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = captureRequest(); |
||||
|
assertThat(requestCaptor.getValue().isPersisted()).isEqualTo(expectedPersistence); |
||||
|
} |
||||
|
|
||||
|
private static Stream<Arguments> givenPersistent_whenOnMsg_thenVerifyRequest() { |
||||
|
return Stream.of( |
||||
|
Arguments.of("true", true), |
||||
|
Arguments.of("false", false), |
||||
|
Arguments.of(null, false), |
||||
|
Arguments.of("", false) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private ArgumentCaptor<RuleEngineDeviceRpcRequest> captureRequest() { |
||||
|
ArgumentCaptor<RuleEngineDeviceRpcRequest> requestCaptor = ArgumentCaptor.forClass(RuleEngineDeviceRpcRequest.class); |
||||
|
then(rpcServiceMock).should().sendRpcRequestToDevice(requestCaptor.capture(), any(Consumer.class)); |
||||
|
return requestCaptor; |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenRpcResponseWithoutError_whenOnMsg_thenSendsRpcRequest() { |
||||
|
TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
|
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
// TODO: replace deprecated method newMsg()
|
||||
|
given(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).willReturn(outMsg); |
||||
|
willAnswer(invocation -> { |
||||
|
Consumer<RuleEngineDeviceRpcResponse> consumer = invocation.getArgument(1); |
||||
|
RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class); |
||||
|
given(rpcResponseMock.getError()).willReturn(Optional.empty()); |
||||
|
given(rpcResponseMock.getResponse()).willReturn(Optional.of(TbMsg.EMPTY_JSON_OBJECT)); |
||||
|
consumer.accept(rpcResponseMock); |
||||
|
return null; |
||||
|
}).given(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class)); |
||||
|
|
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
then(ctxMock).should().enqueueForTellNext(outMsg, TbNodeConnectionType.SUCCESS); |
||||
|
then(ctxMock).should().ack(msg); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
public void givenRpcResponseWithError_whenOnMsg_thenTellFailure() { |
||||
|
TbMsg outMsg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
|
||||
|
given(ctxMock.getRpcService()).willReturn(rpcServiceMock); |
||||
|
given(ctxMock.getTenantId()).willReturn(TENANT_ID); |
||||
|
// TODO: replace deprecated method newMsg()
|
||||
|
given(ctxMock.newMsg(any(), any(String.class), any(), any(), any(), any())).willReturn(outMsg); |
||||
|
willAnswer(invocation -> { |
||||
|
Consumer<RuleEngineDeviceRpcResponse> consumer = invocation.getArgument(1); |
||||
|
RuleEngineDeviceRpcResponse rpcResponseMock = mock(RuleEngineDeviceRpcResponse.class); |
||||
|
given(rpcResponseMock.getError()).willReturn(Optional.of(RpcError.NO_ACTIVE_CONNECTION)); |
||||
|
consumer.accept(rpcResponseMock); |
||||
|
return null; |
||||
|
}).given(rpcServiceMock).sendRpcRequestToDevice(any(RuleEngineDeviceRpcRequest.class), any(Consumer.class)); |
||||
|
|
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.RPC_CALL_FROM_SERVER_TO_DEVICE, DEVICE_ID, TbMsgMetaData.EMPTY, MSG_DATA); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
then(ctxMock).should().enqueueForTellFailure(outMsg, RpcError.NO_ACTIVE_CONNECTION.name()); |
||||
|
then(ctxMock).should().ack(msg); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@EnumSource(EntityType.class) |
||||
|
public void givenOriginatorIsNotDevice_whenOnMsg_thenThrowsException(EntityType entityType) { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, "ac21a1bb-eabf-4463-8313-24bea1f498d9"); |
||||
|
|
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, entityId, TbMsgMetaData.EMPTY, TbMsg.EMPTY_JSON_OBJECT); |
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class); |
||||
|
then(ctxMock).should().tellFailure(eq(msg), throwableCaptor.capture()); |
||||
|
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class) |
||||
|
.hasMessage(EntityType.DEVICE != entityType ? "Message originator is not a device entity!" |
||||
|
: "Method is not present in the message!"); |
||||
|
} |
||||
|
|
||||
|
@ParameterizedTest |
||||
|
@ValueSource(strings = {"method", "params"}) |
||||
|
public void givenMethodOrParamsAreNotPresent_whenOnMsg_thenThrowsException(String key) { |
||||
|
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, DEVICE_ID, TbMsgMetaData.EMPTY, "{\"" + key + "\": \"value\"}"); |
||||
|
|
||||
|
node.onMsg(ctxMock, msg); |
||||
|
|
||||
|
ArgumentCaptor<Throwable> throwableCaptor = ArgumentCaptor.forClass(Throwable.class); |
||||
|
then(ctxMock).should().tellFailure(eq(msg), throwableCaptor.capture()); |
||||
|
assertThat(throwableCaptor.getValue()).isInstanceOf(RuntimeException.class) |
||||
|
.hasMessage(key.equals("method") ? "Params are not present in the message!" : "Method is not present in the message!"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2024 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div class="tb-form-row space-between same-padding tb-flex column" [formGroup]="securityFormGroup"> |
||||
|
<div class="tb-flex row space-between align-center no-gap fill-width"> |
||||
|
<div class="fields-label" translate>gateway.security</div> |
||||
|
<tb-toggle-select formControlName="type" appearance="fill"> |
||||
|
<tb-toggle-option *ngFor="let type of securityTypes" [value]="type"> |
||||
|
{{ SecurityTypeTranslationsMap.get(type) | translate }} |
||||
|
</tb-toggle-option> |
||||
|
</tb-toggle-select> |
||||
|
</div> |
||||
|
<ng-container *ngIf="securityFormGroup.get('type').value === BrokerSecurityType.BASIC"> |
||||
|
<div class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.username</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput name="value" formControlName="username" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.username-required') | translate" |
||||
|
*ngIf="securityFormGroup.get('username').hasError('required') && securityFormGroup.get('username').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="tb-form-row space-between tb-flex fill-width"> |
||||
|
<div class="fixed-title-width" translate>gateway.password</div> |
||||
|
<div class="tb-flex no-gap"> |
||||
|
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input matInput type="password" name="value" formControlName="password" placeholder="{{ 'gateway.set' | translate }}"/> |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="('gateway.password-required') | translate" |
||||
|
*ngIf="securityFormGroup.get('password').hasError('required') |
||||
|
&& securityFormGroup.get('password').touched" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
<div [class.hide-toggle]="securityFormGroup.get('password').hasError('required')" class="tb-flex no-gap align-center fill-height" matSuffix> |
||||
|
<tb-toggle-password class="tb-flex align-center fill-height"></tb-toggle-password> |
||||
|
</div> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ng-container> |
||||
|
</div> |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2024 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
display: block; |
||||
|
margin-bottom: 10px; |
||||
|
|
||||
|
.fields-label { |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.hide-toggle { |
||||
|
display: none; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2024 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
Component, |
||||
|
forwardRef, |
||||
|
OnDestroy, |
||||
|
} from '@angular/core'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
UntypedFormGroup, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
import { |
||||
|
noLeadTrailSpacesRegex, |
||||
|
RestSecurityType, |
||||
|
RestSecurityTypeTranslationsMap |
||||
|
} from '@home/components/widget/lib/gateway/gateway-widget.models'; |
||||
|
import { SharedModule } from '@shared/shared.module'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-rest-connector-security', |
||||
|
templateUrl: './rest-connector-security.component.html', |
||||
|
styleUrls: ['./rest-connector-security.component.scss'], |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => RestConnectorSecurityComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => RestConnectorSecurityComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
], |
||||
|
standalone: true, |
||||
|
imports: [ |
||||
|
SharedModule, |
||||
|
CommonModule, |
||||
|
] |
||||
|
}) |
||||
|
export class RestConnectorSecurityComponent implements ControlValueAccessor, Validator, OnDestroy { |
||||
|
BrokerSecurityType = RestSecurityType; |
||||
|
securityTypes: RestSecurityType[] = Object.values(RestSecurityType); |
||||
|
SecurityTypeTranslationsMap = RestSecurityTypeTranslationsMap; |
||||
|
securityFormGroup: UntypedFormGroup; |
||||
|
|
||||
|
private destroy$ = new Subject<void>(); |
||||
|
private propagateChange = (_: any) => {}; |
||||
|
|
||||
|
constructor(private fb: FormBuilder) { |
||||
|
this.securityFormGroup = this.fb.group({ |
||||
|
type: [RestSecurityType.ANONYMOUS, []], |
||||
|
username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
password: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], |
||||
|
}); |
||||
|
this.observeSecurityForm(); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
this.destroy$.next(); |
||||
|
this.destroy$.complete(); |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void {} |
||||
|
|
||||
|
writeValue(deviceInfo: any): void { |
||||
|
if (!deviceInfo.type) { |
||||
|
deviceInfo.type = RestSecurityType.ANONYMOUS; |
||||
|
} |
||||
|
this.securityFormGroup.reset(deviceInfo); |
||||
|
this.updateView(deviceInfo); |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.securityFormGroup.valid ? null : { |
||||
|
securityForm: { valid: false } |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private updateView(value: any): void { |
||||
|
this.propagateChange(value); |
||||
|
} |
||||
|
|
||||
|
private updateValidators(type: RestSecurityType): void { |
||||
|
if (type === RestSecurityType.BASIC) { |
||||
|
this.securityFormGroup.get('username').enable({emitEvent: false}); |
||||
|
this.securityFormGroup.get('password').enable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.securityFormGroup.get('username').disable({emitEvent: false}); |
||||
|
this.securityFormGroup.get('password').disable({emitEvent: false}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private observeSecurityForm(): void { |
||||
|
this.securityFormGroup.valueChanges |
||||
|
.pipe(takeUntil(this.destroy$)) |
||||
|
.subscribe(value => this.updateView(value)); |
||||
|
|
||||
|
this.securityFormGroup.get('type').valueChanges |
||||
|
.pipe(takeUntil(this.destroy$)) |
||||
|
.subscribe(type => this.updateValidators(type)); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue