diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml
index f06b4a7395..c0b1f409de 100644
--- a/rule-engine/rule-engine-components/pom.xml
+++ b/rule-engine/rule-engine-components/pom.xml
@@ -136,6 +136,17 @@
awaitility
test
+
+ org.mock-server
+ mockserver-netty
+ 5.13.1
+
+
+ org.mock-server
+ mockserver-client-java
+ 5.13.1
+
+
org.cassandraunit
cassandra-unit
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
index 7d8a670b60..e035d415ed 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
@@ -191,7 +191,7 @@ public class TbHttpClient {
entity = new HttpEntity<>(msg.getData(), headers);
}
- URI uri = UriComponentsBuilder.fromUriString(endpointUrl).build().encode().toUri();
+ URI uri = buildEncodedUri(endpointUrl);
ListenableFuture> future = httpClient.exchange(
uri, method, entity, String.class);
future.addCallback(new ListenableFutureCallback>() {
@@ -217,6 +217,28 @@ public class TbHttpClient {
}
}
+ public URI buildEncodedUri(String endpointUrl) {
+ if (endpointUrl == null) {
+ throw new RuntimeException("Url string cannot be null!");
+ }
+ if (endpointUrl.isEmpty()) {
+ throw new RuntimeException("Url string cannot be empty!");
+ }
+
+ URI uri = UriComponentsBuilder.fromUriString(endpointUrl).build().encode().toUri();
+ if (uri.getScheme() == null || uri.getScheme().isEmpty()) {
+ throw new RuntimeException("Transport scheme(protocol) must be provided!");
+ }
+
+ boolean authorityNotValid = uri.getAuthority() == null || uri.getAuthority().isEmpty();
+ boolean hostNotValid = uri.getHost() == null || uri.getHost().isEmpty();
+ if (authorityNotValid || hostNotValid) {
+ throw new RuntimeException("Url string is invalid!");
+ }
+
+ return uri;
+ }
+
private TbMsg processResponse(TbContext ctx, TbMsg origMsg, ResponseEntity response) {
TbMsgMetaData metaData = origMsg.getMetaData();
metaData.putValue(STATUS, response.getStatusCode().name());
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
index 1fb30b262a..f5c6977dd5 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
@@ -18,19 +18,15 @@ package org.thingsboard.rule.engine.rest;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
+import org.awaitility.Awaitility;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.scheduling.annotation.AsyncResult;
+import org.mockito.Mockito;
+import org.mockserver.integration.ClientAndServer;
import org.springframework.web.client.AsyncRestTemplate;
-import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
@@ -38,7 +34,9 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.net.URI;
+import java.util.concurrent.TimeUnit;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
@@ -49,15 +47,15 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static org.mockserver.integration.ClientAndServer.startClientAndServer;
+import static org.mockserver.model.HttpRequest.request;
+import static org.mockserver.model.HttpResponse.response;
public class TbHttpClientTest {
EventLoopGroup eventLoop;
TbHttpClient client;
- private final String ENDPOINT_URL = "http://localhost/api?data=[{\\\"test\\\":\\\"test\\\"}]";
- private final String GET_METHOD = "GET";
-
@Before
public void setUp() throws Exception {
client = mock(TbHttpClient.class);
@@ -83,29 +81,61 @@ public class TbHttpClientTest {
assertThat(eventLoop, instanceOf(NioEventLoopGroup.class));
}
+ @Test
+ public void testBuildSimpleUri() {
+ Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod();
+ String url = "http://localhost:8080/";
+ URI uri = client.buildEncodedUri(url);
+ Assert.assertEquals(url, uri.toString());
+ }
+
+ @Test
+ public void testBuildUriWithoutProtocol() {
+ Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod();
+ String url = "localhost:8080/";
+ assertThatThrownBy(() -> client.buildEncodedUri(url));
+ }
+
+ @Test
+ public void testBuildInvalidUri() {
+ Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod();
+ String url = "aaa";
+ assertThatThrownBy(() -> client.buildEncodedUri(url));
+ }
+
+ @Test
+ public void testBuildUriWithSpecialSymbols() {
+ Mockito.when(client.buildEncodedUri(any())).thenCallRealMethod();
+ String url = "http://192.168.1.1/data?d={\"a\": 12}";
+ String expected = "http://192.168.1.1/data?d=%7B%22a%22:%2012%7D";
+ URI uri = client.buildEncodedUri(url);
+ Assert.assertEquals(expected, uri.toString());
+ }
+
@Test
public void testProcessMessageWithJsonInUrlVariable() throws Exception {
- var config = new TbRestApiCallNodeConfiguration()
- .defaultConfiguration();
- config.setRequestMethod(GET_METHOD);
- config.setRestEndpointUrlPattern(ENDPOINT_URL);
- config.setUseSimpleClientHttpFactory(true);
+ String host = "localhost";
+ String path = "/api";
+ String paramKey = "data";
+ String paramVal = "[{\"test\":\"test\"}]";
+ String successResponseBody = "SUCCESS";
- var asyncRestTemplate = mock(AsyncRestTemplate.class);
- var uriCaptor = ArgumentCaptor.forClass(URI.class);
+ var server = setUpDummyServer(host, path, paramKey, paramVal, successResponseBody);
- var responseEntity = new ResponseEntity<>(
- "{}",
- new HttpHeaders(),
- HttpStatus.OK
+ String endpointUrl = String.format(
+ "http://%s:%d%s?%s=%s",
+ host, server.getPort(), path, paramKey, paramVal
);
+ String method = "GET";
- when(asyncRestTemplate.exchange(
- uriCaptor.capture(),
- any(),
- any(),
- eq(String.class)
- )).thenReturn(new AsyncResult<>(responseEntity));
+
+ var config = new TbRestApiCallNodeConfiguration()
+ .defaultConfiguration();
+ config.setRequestMethod(method);
+ config.setRestEndpointUrlPattern(endpointUrl);
+ config.setUseSimpleClientHttpFactory(true);
+
+ var asyncRestTemplate = new AsyncRestTemplate();
var httpClient = new TbHttpClient(config, eventLoop);
httpClient.setHttpClient(asyncRestTemplate);
@@ -121,24 +151,57 @@ public class TbHttpClientTest {
var ctx = mock(TbContext.class);
when(ctx.transformMsg(
- eq(msg), eq(msg.getType()),
- eq(msg.getOriginator()),
- eq(msg.getMetaData()),
- eq(msg.getData())
- )).thenReturn(successMsg);
-
- httpClient.processMessage(ctx, msg);
-
- verify(ctx, times(1)).transformMsg(
eq(msg), eq(msg.getType()),
eq(msg.getOriginator()),
eq(msg.getMetaData()),
eq(msg.getData())
- );
- verify(ctx, times(1))
- .tellSuccess(eq(successMsg));
+ )).thenReturn(successMsg);
- URI uri = UriComponentsBuilder.fromUriString(ENDPOINT_URL).build().encode().toUri();
- Assert.assertEquals("URI encoding was not performed!!", uri, uriCaptor.getValue());
+ var capturedData = ArgumentCaptor.forClass(String.class);
+
+ when(ctx.transformMsg(
+ eq(msg), eq(msg.getType()),
+ eq(msg.getOriginator()),
+ any(),
+ capturedData.capture()
+ )).thenReturn(successMsg);
+
+ httpClient.processMessage(ctx, msg);
+
+ Awaitility.await()
+ .atMost(30, TimeUnit.SECONDS)
+ .until(() -> {
+ try {
+ verify(ctx, times(1)).tellSuccess(any());
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ });
+
+ verify(ctx, times(1)).tellSuccess(any());
+ verify(ctx, times(0)).tellFailure(any(), any());
+ Assert.assertEquals(successResponseBody, capturedData.getValue());
+ }
+
+ private ClientAndServer setUpDummyServer(String host, String path, String paramKey, String paramVal, String successResponseBody) {
+ var server = startClientAndServer(host, 1080);
+ createGetMethodExpectations(server, path, paramKey, paramVal, successResponseBody);
+ return server;
}
+
+ private void createGetMethodExpectations(ClientAndServer server, String path, String paramKey, String paramVal, String successResponseBody) {
+ server.when(
+ request()
+ .withMethod("GET")
+ .withPath(path)
+ .withQueryStringParameter(paramKey, paramVal)
+ ).respond(
+ response()
+ .withStatusCode(200)
+ .withBody(successResponseBody)
+ );
+ }
+
+
}
\ No newline at end of file