Commit 71e098c8c41b2c0c2bcf7b00b5bfdfa50da40e59

Authored by Andrew Shvayka
Committed by GitHub
2 parents da07edba e884724e

Merge pull request #5304 from msqr/master

Add option for HTTP client rule node to not create any message body
@@ -182,7 +182,8 @@ public class TbHttpClient { @@ -182,7 +182,8 @@ public class TbHttpClient {
182 HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); 182 HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
183 HttpEntity<String> entity; 183 HttpEntity<String> entity;
184 if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) || 184 if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) ||
185 - HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method)) { 185 + HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method) ||
  186 + config.isIgnoreRequestBody()) {
186 entity = new HttpEntity<>(headers); 187 entity = new HttpEntity<>(headers);
187 } else { 188 } else {
188 entity = new HttpEntity<>(msg.getData(), headers); 189 entity = new HttpEntity<>(msg.getData(), headers);
@@ -47,6 +47,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA @@ -47,6 +47,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
47 private String proxyPassword; 47 private String proxyPassword;
48 private String proxyScheme; 48 private String proxyScheme;
49 private ClientCredentials credentials; 49 private ClientCredentials credentials;
  50 + private boolean ignoreRequestBody;
50 51
51 @Override 52 @Override
52 public TbRestApiCallNodeConfiguration defaultConfiguration() { 53 public TbRestApiCallNodeConfiguration defaultConfiguration() {
@@ -61,6 +62,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA @@ -61,6 +62,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
61 configuration.setTrimQueue(false); 62 configuration.setTrimQueue(false);
62 configuration.setEnableProxy(false); 63 configuration.setEnableProxy(false);
63 configuration.setCredentials(new AnonymousCredentials()); 64 configuration.setCredentials(new AnonymousCredentials());
  65 + configuration.setIgnoreRequestBody(false);
64 return configuration; 66 return configuration;
65 } 67 }
66 68
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.rest;
  17 +
  18 +import static org.junit.Assert.*;
  19 +import static org.mockito.Mockito.verify;
  20 +
  21 +import java.io.IOException;
  22 +import java.util.Collections;
  23 +import java.util.concurrent.CountDownLatch;
  24 +import java.util.concurrent.TimeUnit;
  25 +
  26 +import org.apache.http.Header;
  27 +import org.apache.http.HttpException;
  28 +import org.apache.http.HttpRequest;
  29 +import org.apache.http.HttpResponse;
  30 +import org.apache.http.config.SocketConfig;
  31 +import org.apache.http.impl.bootstrap.HttpServer;
  32 +import org.apache.http.impl.bootstrap.ServerBootstrap;
  33 +import org.apache.http.protocol.HttpContext;
  34 +import org.apache.http.protocol.HttpRequestHandler;
  35 +import org.junit.After;
  36 +import org.junit.Test;
  37 +import org.junit.runner.RunWith;
  38 +import org.mockito.ArgumentCaptor;
  39 +import org.mockito.Mock;
  40 +import org.mockito.junit.MockitoJUnitRunner;
  41 +import org.thingsboard.rule.engine.api.TbContext;
  42 +import org.thingsboard.rule.engine.api.TbEmail;
  43 +import org.thingsboard.rule.engine.api.TbNodeConfiguration;
  44 +import org.thingsboard.rule.engine.api.TbNodeException;
  45 +import org.thingsboard.server.common.data.id.DeviceId;
  46 +import org.thingsboard.server.common.data.id.EntityId;
  47 +import org.thingsboard.server.common.data.id.RuleChainId;
  48 +import org.thingsboard.server.common.data.id.RuleNodeId;
  49 +import org.thingsboard.server.common.msg.TbMsg;
  50 +import org.thingsboard.server.common.msg.TbMsgDataType;
  51 +import org.thingsboard.server.common.msg.TbMsgMetaData;
  52 +
  53 +import com.datastax.oss.driver.api.core.uuid.Uuids;
  54 +import com.fasterxml.jackson.databind.ObjectMapper;
  55 +
  56 +@RunWith(MockitoJUnitRunner.class)
  57 +public class TbRestApiCallNodeTest {
  58 +
  59 + private TbRestApiCallNode restNode;
  60 +
  61 + @Mock
  62 + private TbContext ctx;
  63 +
  64 + private EntityId originator = new DeviceId(Uuids.timeBased());
  65 + private TbMsgMetaData metaData = new TbMsgMetaData();
  66 +
  67 + private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased());
  68 + private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
  69 +
  70 + private HttpServer server;
  71 +
  72 + public void setupServer(String pattern, HttpRequestHandler handler) throws IOException {
  73 + SocketConfig config = SocketConfig.custom().setSoReuseAddress(true).setTcpNoDelay(true).build();
  74 + server = ServerBootstrap.bootstrap()
  75 + .setSocketConfig(config)
  76 + .registerHandler(pattern, handler)
  77 + .create();
  78 + server.start();
  79 + }
  80 +
  81 + private void initWithConfig(TbRestApiCallNodeConfiguration config) {
  82 + try {
  83 + ObjectMapper mapper = new ObjectMapper();
  84 + TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
  85 + restNode = new TbRestApiCallNode();
  86 + restNode.init(ctx, nodeConfiguration);
  87 + } catch (TbNodeException ex) {
  88 + throw new IllegalStateException(ex);
  89 + }
  90 + }
  91 +
  92 + @After
  93 + public void teardown() {
  94 + server.stop();
  95 + }
  96 +
  97 + @Test
  98 + public void deleteRequestWithoutBody() throws IOException, InterruptedException {
  99 + final CountDownLatch latch = new CountDownLatch(1);
  100 + final String path = "/path/to/delete";
  101 + setupServer("*", new HttpRequestHandler() {
  102 +
  103 + @Override
  104 + public void handle(HttpRequest request, HttpResponse response, HttpContext context)
  105 + throws HttpException, IOException {
  106 + try {
  107 + assertEquals("Request path matches", request.getRequestLine().getUri(), path);
  108 + assertFalse("Content-Type not included", request.containsHeader("Content-Type"));
  109 + assertTrue("Custom header included", request.containsHeader("Foo"));
  110 + assertEquals("Custom header value", "Bar", request.getFirstHeader("Foo").getValue());
  111 + response.setStatusCode(200);
  112 + new Thread(new Runnable() {
  113 + @Override
  114 + public void run() {
  115 + try {
  116 + Thread.sleep(1000L);
  117 + } catch (InterruptedException e) {
  118 + // ignore
  119 + } finally {
  120 + latch.countDown();
  121 + }
  122 + }
  123 + }).start();
  124 + } catch ( Exception e ) {
  125 + System.out.println("Exception handling request: " + e.toString());
  126 + e.printStackTrace();
  127 + latch.countDown();
  128 + }
  129 + }
  130 + });
  131 +
  132 + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
  133 + config.setRequestMethod("DELETE");
  134 + config.setHeaders(Collections.singletonMap("Foo", "Bar"));
  135 + config.setIgnoreRequestBody(true);
  136 + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path));
  137 + initWithConfig(config);
  138 +
  139 + TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
  140 + restNode.onMsg(ctx, msg);
  141 +
  142 + assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS));
  143 +
  144 + ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
  145 + ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
  146 + ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
  147 + ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
  148 + ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
  149 + verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
  150 +
  151 +
  152 + assertEquals("USER", typeCaptor.getValue());
  153 + assertEquals(originator, originatorCaptor.getValue());
  154 + assertNotSame(metaData, metadataCaptor.getValue());
  155 + assertEquals("{}", dataCaptor.getValue());
  156 + }
  157 +
  158 + @Test
  159 + public void deleteRequestWithBody() throws IOException, InterruptedException {
  160 + final CountDownLatch latch = new CountDownLatch(1);
  161 + final String path = "/path/to/delete";
  162 + setupServer("*", new HttpRequestHandler() {
  163 +
  164 + @Override
  165 + public void handle(HttpRequest request, HttpResponse response, HttpContext context)
  166 + throws HttpException, IOException {
  167 + try {
  168 + assertEquals("Request path matches", path, request.getRequestLine().getUri());
  169 + assertTrue("Content-Type included", request.containsHeader("Content-Type"));
  170 + assertEquals("Content-Type value", "text/plain;charset=ISO-8859-1",
  171 + request.getFirstHeader("Content-Type").getValue());
  172 + assertTrue("Content-Length included", request.containsHeader("Content-Length"));
  173 + assertEquals("Content-Length value", "2",
  174 + request.getFirstHeader("Content-Length").getValue());
  175 + assertTrue("Custom header included", request.containsHeader("Foo"));
  176 + assertEquals("Custom header value", "Bar", request.getFirstHeader("Foo").getValue());
  177 + response.setStatusCode(200);
  178 + new Thread(new Runnable() {
  179 + @Override
  180 + public void run() {
  181 + try {
  182 + Thread.sleep(1000L);
  183 + } catch (InterruptedException e) {
  184 + // ignore
  185 + } finally {
  186 + latch.countDown();
  187 + }
  188 + }
  189 + }).start();
  190 + } catch ( Exception e ) {
  191 + System.out.println("Exception handling request: " + e.toString());
  192 + e.printStackTrace();
  193 + latch.countDown();
  194 + }
  195 + }
  196 + });
  197 +
  198 + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
  199 + config.setRequestMethod("DELETE");
  200 + config.setHeaders(Collections.singletonMap("Foo", "Bar"));
  201 + config.setIgnoreRequestBody(false);
  202 + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path));
  203 + initWithConfig(config);
  204 +
  205 + TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
  206 + restNode.onMsg(ctx, msg);
  207 +
  208 + assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS));
  209 +
  210 + ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
  211 + ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
  212 + ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
  213 + ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
  214 + ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
  215 + verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
  216 +
  217 + assertEquals("USER", typeCaptor.getValue());
  218 + assertEquals(originator, originatorCaptor.getValue());
  219 + assertNotSame(metaData, metadataCaptor.getValue());
  220 + assertEquals("{}", dataCaptor.getValue());
  221 + }
  222 +
  223 +}