Commit 71e098c8c41b2c0c2bcf7b00b5bfdfa50da40e59
Committed by
GitHub
Merge pull request #5304 from msqr/master
Add option for HTTP client rule node to not create any message body
Showing
3 changed files
with
227 additions
and
1 deletions
@@ -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 | +} |