Browse Source

REST API call node: support duplicate query param names

pull/14540/head
Dmytro Skarzhynets 5 months ago
parent
commit
e0fbbf6b4e
No known key found for this signature in database GPG Key ID: 2B51652F224037DF
  1. 10
      common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
  2. 23
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/QueryParam.java
  3. 27
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  4. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
  5. 146
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java
  6. 27
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java

10
common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java

@ -18,7 +18,6 @@ package org.thingsboard.server.common.data.util;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -73,15 +72,6 @@ public class CollectionsUtil {
return map;
}
@SafeVarargs
public static <K, V> LinkedHashMap<K, V> orderedMapOf(Map.Entry<K, V>... entries) {
LinkedHashMap<K, V> map = new LinkedHashMap<>();
for (Map.Entry<K, V> entry : entries) {
map.put(entry.getKey(), entry.getValue());
}
return map;
}
public static <V> boolean emptyOrContains(Collection<V> collection, V element) {
return isEmpty(collection) || collection.contains(element);
}

23
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/QueryParam.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2025 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.rest;
import jakarta.validation.constraints.NotNull;
record QueryParam(
@NotNull(message = "query parameter names must be non-null") String name,
@NotNull(message = "query parameter values must be non-null") String value
) {}

27
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.rest;
import com.google.common.collect.Maps;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.ssl.SslContext;
@ -217,14 +216,15 @@ public class TbHttpClient {
String endpointUrl = TbNodeUtils.processPattern(config.getRestEndpointUrlPattern(), msg);
Map<String, String> processedQueryParams;
List<QueryParam> processedQueryParams;
if (config.isUseNewEncoding()) {
processedQueryParams = Maps.newHashMapWithExpectedSize(config.getQueryParams().size());
config.getQueryParams().forEach((name, value) -> {
var processedParamName = TbNodeUtils.processPattern(name, msg);
var processedParamValue = TbNodeUtils.processPattern(value, msg);
processedQueryParams.put(processedParamName, processedParamValue);
});
processedQueryParams = config.getQueryParams().stream()
.map(param -> {
var processedParamName = TbNodeUtils.processPattern(param.name(), msg);
var processedParamValue = TbNodeUtils.processPattern(param.value(), msg);
return new QueryParam(processedParamName, processedParamValue);
})
.toList();
} else {
processedQueryParams = null;
}
@ -278,7 +278,7 @@ public class TbHttpClient {
return origin;
}
public URI buildEncodedUri(String endpointUrl, Map<String, String> queryParams) {
public URI buildEncodedUri(String endpointUrl, List<QueryParam> queryParams) {
if (endpointUrl == null) {
throw new RuntimeException("Url string cannot be null!");
}
@ -306,16 +306,15 @@ public class TbHttpClient {
return uri;
}
private URI buildEncodedUriNew(String endpointUrl, Map<String, String> queryParams) {
private URI buildEncodedUriNew(String endpointUrl, List<QueryParam> queryParams) {
try {
URIBuilder builder = new URIBuilder(endpointUrl);
queryParams.forEach(builder::addParameter);
queryParams.forEach(param -> builder.addParameter(param.name(), param.value()));
return builder.build();
} catch (URISyntaxException e) {
throw new IllegalArgumentException("""
Invalid REST endpoint URL: '%s'. The URL must be valid and properly encoded.
If the path contains special characters (e.g., spaces, braces), they must be percent-encoded (e.g., '/my file/' should be '/my%%20file/', '/path/{id}/' should be '/path/%%7Bid%%7D/').
Query parameters should be configured separately and will be encoded automatically.""".formatted(endpointUrl), e);
Invalid request URL: '%s'. The URL must be valid and properly encoded.
If URL contains special characters (e.g., spaces), they must be percent-encoded (e.g., '/my file/' should be '/my%%20file/').""".formatted(endpointUrl), e);
}
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java

@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.rest;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@ -27,6 +28,7 @@ import org.thingsboard.rule.engine.credentials.AnonymousCredentials;
import org.thingsboard.rule.engine.credentials.ClientCredentials;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@JsonIgnoreProperties(ignoreUnknown = true)
@ -36,7 +38,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
private String restEndpointUrlPattern;
private String requestMethod;
private boolean useNewEncoding;
private Map<@NotNull String, @NotNull(message = "query parameter values must be non-null") String> queryParams;
private List<@NotNull @Valid QueryParam> queryParams;
private Map<String, String> headers;
private boolean useSimpleClientHttpFactory;
private int readTimeoutMs;
@ -78,7 +80,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
configuration.setRequestMethod("POST");
configuration.setHeaders(Collections.singletonMap(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE));
configuration.setUseNewEncoding(true);
configuration.setQueryParams(Collections.emptyMap());
configuration.setQueryParams(Collections.emptyList());
configuration.setUseSimpleClientHttpFactory(false);
configuration.setReadTimeoutMs(0);
configuration.setMaxParallelRequestsCount(0);

146
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java

@ -44,7 +44,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
@ -61,7 +60,6 @@ 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;
import static org.thingsboard.server.common.data.util.CollectionsUtil.orderedMapOf;
public class TbHttpClientTest {
@ -147,12 +145,12 @@ public class TbHttpClientTest {
config.setRequestMethod("GET");
config.setUseSimpleClientHttpFactory(true);
config.setUseNewEncoding(true);
config.setQueryParams(Map.of(
"email", "${userEmail}", // ${} from metadata
"device", "${deviceName}", // ${} from metadata
"${dynamicParam}", "${dynamicValue}", // ${} in both key and value
"temp", "$[temperature]", // $[] from data
"location", "$[sensor.location]" // $[] from nested data
config.setQueryParams(List.of(
new QueryParam("email", "${userEmail}"), // ${} from metadata
new QueryParam("device", "${deviceName}"), // ${} from metadata
new QueryParam("${dynamicParam}", "${dynamicValue}"), // ${} in both key and value
new QueryParam("temp", "$[temperature]"), // $[] from data
new QueryParam("location", "$[sensor.location]") // $[] from nested data
));
var metaData = new TbMsgMetaData();
@ -238,7 +236,7 @@ public class TbHttpClientTest {
@ParameterizedTest(name = "{0}")
@MethodSource
public void testQueryParamsEncoding(String endpointUrl, Map<String, String> queryParams, String expectedEncodedUrl) {
public void testQueryParamsEncoding(String endpointUrl, List<QueryParam> queryParams, String expectedEncodedUrl) {
// GIVEN
Mockito.when(client.buildEncodedUri(any(), any())).thenCallRealMethod();
@ -253,42 +251,42 @@ public class TbHttpClientTest {
return Stream.of(
Arguments.of(
Named.named("ISO 8601 date-time in value", "http://somecompany/api/data/fetch"),
Map.of("ts", "2016-08-01T09:06:06.0+02:00"),
List.of(new QueryParam("ts", "2016-08-01T09:06:06.0+02:00")),
"http://somecompany/api/data/fetch?ts=2016-08-01T09%3A06%3A06.0%2B02%3A00"
),
Arguments.of(
Named.named("email with plus sign in value", "http://localhost:8080/api/user/sendActivationMail"),
Map.of("email", "someperson+test1289@thingsboard.io"),
List.of(new QueryParam("email", "someperson+test1289@thingsboard.io")),
"http://localhost:8080/api/user/sendActivationMail?email=someperson%2Btest1289%40thingsboard.io"
),
Arguments.of(
Named.named("plus mixed with spaces in value", "http://url/api"),
Map.of("q", "a + b"),
List.of(new QueryParam("q", "a + b")),
"http://url/api?q=a%20%2B%20b"
),
Arguments.of(
Named.named("colon in value", "http://url/api"),
Map.of("time", "12:00"),
List.of(new QueryParam("time", "12:00")),
"http://url/api?time=12%3A00"
),
Arguments.of(
Named.named("slash in value", "http://url"),
Map.of("ref", "/home/user"),
List.of(new QueryParam("ref", "/home/user")),
"http://url?ref=%2Fhome%2Fuser"
),
Arguments.of(
Named.named("comma and semicolon in value", "http://url"),
Map.of("l", "a,b;c"),
List.of(new QueryParam("l", "a,b;c")),
"http://url?l=a%2Cb%3Bc"
),
Arguments.of(
Named.named("ampersand and equals in value", "http://url"),
Map.of("q", "key1=value1&key2=value2"),
List.of(new QueryParam("q", "key1=value1&key2=value2")),
"http://url?q=key1%3Dvalue1%26key2%3Dvalue2"
),
Arguments.of(
Named.named("JSON in value", "http://url"),
Map.of("json", """
List.of(new QueryParam("json", """
{
"string": "hello",
"integer": 42,
@ -305,245 +303,253 @@ public class TbHttpClientTest {
"object": {
"nested": "value"
}
}"""),
}""")),
"http://url?json=%7B%0A%20%20%20%20%22string%22%3A%20%22hello%22%2C%0A%20%20%20%20%22integer%22%3A%2042%2C%0A%20%20%20%20%22float%22%3A%203.14%2C%0A%20%20%20%20%22boolTrue%22%3A%20true%2C%0A%20%20%20%20%22boolFalse%22%3A%20false%2C%0A%20%20%20%20%22null%22%3A%20null%2C%0A%20%20%20%20%22array%22%3A%20%5B%0A%20%20%20%20%20%20%20%201%2C%0A%20%20%20%20%20%20%20%20%22two%22%2C%0A%20%20%20%20%20%20%20%20true%2C%0A%20%20%20%20%20%20%20%20null%0A%20%20%20%20%5D%2C%0A%20%20%20%20%22object%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22nested%22%3A%20%22value%22%0A%20%20%20%20%7D%0A%7D"
),
Arguments.of(
Named.named("UTF-8 in query", "http://url/cafes"),
orderedMapOf(
entry("nom", "Le Goût Moderne"),
entry("назва", "У Миколи \uD83D\uDE0B")
List.of(
new QueryParam("nom", "Le Goût Moderne"),
new QueryParam("назва", "У Миколи \uD83D\uDE0B")
),
"http://url/cafes?nom=Le%20Go%C3%BBt%20Moderne&%D0%BD%D0%B0%D0%B7%D0%B2%D0%B0=%D0%A3%20%D0%9C%D0%B8%D0%BA%D0%BE%D0%BB%D0%B8%20%F0%9F%98%8B"
),
Arguments.of(
Named.named("empty value", "http://url/empty"),
Map.of("name", ""),
List.of(new QueryParam("name", "")),
"http://url/empty?name="
),
Arguments.of(
Named.named("blank value (spaces)", "http://url/empty"),
Map.of("name", " "),
List.of(new QueryParam("name", " ")),
"http://url/empty?name=%20%20"
),
Arguments.of(
Named.named("empty key", "http://url/empty"),
Map.of("", "value"),
List.of(new QueryParam("", "value")),
"http://url/empty?=value"
),
Arguments.of(
Named.named("blank key (spaces)", "http://url/empty"),
Map.of(" ", "value"),
List.of(new QueryParam(" ", "value")),
"http://url/empty?%20%20=value"
),
Arguments.of(
Named.named("blank key with value that needs to be encoded", "http://url/empty"),
Map.of(" ", "value1+value2"),
List.of(new QueryParam(" ", "value1+value2")),
"http://url/empty?%20%20=value1%2Bvalue2"
),
Arguments.of(
Named.named("blank key and value", "http://url/empty"),
Map.of(" ", " "),
List.of(new QueryParam(" ", " ")),
"http://url/empty?%20%20=%20%20"
),
Arguments.of(
Named.named("fragment with query params", "http://url#frag"),
Map.of("docs", "مستندات عقدة القاعدة\n"),
List.of(new QueryParam("docs", "مستندات عقدة القاعدة\n")),
"http://url?docs=%D9%85%D8%B3%D8%AA%D9%86%D8%AF%D8%A7%D8%AA%20%D8%B9%D9%82%D8%AF%D8%A9%20%D8%A7%D9%84%D9%82%D8%A7%D8%B9%D8%AF%D8%A9%0A#frag"
),
Arguments.of(
Named.named("fragment only", "http://url#frag"),
Collections.emptyMap(),
Collections.emptyList(),
"http://url#frag"
),
Arguments.of(
Named.named("multiple query params (ordered)", "http://url/api"),
orderedMapOf(
entry("param1", "value1"),
entry("param2", "value2"),
entry("param3", "value3")
List.of(
new QueryParam("param1", "value1"),
new QueryParam("param2", "value2"),
new QueryParam("param3", "value3")
),
"http://url/api?param1=value1&param2=value2&param3=value3"
),
Arguments.of(
Named.named("hash (#) in value", "http://url/api"),
Map.of("color", "#ff0000"),
List.of(new QueryParam("color", "#ff0000")),
"http://url/api?color=%23ff0000"
),
Arguments.of(
Named.named("question mark (?) in value", "http://url/api"),
Map.of("query", "what?"),
List.of(new QueryParam("query", "what?")),
"http://url/api?query=what%3F"
),
Arguments.of(
Named.named("percent sign (%) in value", "http://url/api"),
Map.of("discount", "50%"),
List.of(new QueryParam("discount", "50%")),
"http://url/api?discount=50%25"
),
Arguments.of(
Named.named("already encoded string - double encoding", "http://url/api"),
Map.of("encoded", "%20"),
List.of(new QueryParam("encoded", "%20")),
"http://url/api?encoded=%2520"
),
Arguments.of(
Named.named("URL with port number", "http://localhost:8080/api/v1/data"),
Map.of("key", "value"),
List.of(new QueryParam("key", "value")),
"http://localhost:8080/api/v1/data?key=value"
),
Arguments.of(
Named.named("URL with userinfo (auth)", "http://user:password@hostname/path"),
Map.of("secure", "true"),
List.of(new QueryParam("secure", "true")),
"http://user:password@hostname/path?secure=true"
),
Arguments.of(
Named.named("IPv4 address in URL", "http://192.168.1.100:9090/endpoint"),
Map.of("ip", "test"),
List.of(new QueryParam("ip", "test")),
"http://192.168.1.100:9090/endpoint?ip=test"
),
Arguments.of(
Named.named("IPv6 address in URL", "http://[::1]:8080/api"),
Map.of("ipv6", "true"),
List.of(new QueryParam("ipv6", "true")),
"http://[::1]:8080/api?ipv6=true"
),
Arguments.of(
Named.named("HTTPS protocol", "https://secure.example.com/api"),
Map.of("token", "abc123"),
List.of(new QueryParam("token", "abc123")),
"https://secure.example.com/api?token=abc123"
),
Arguments.of(
Named.named("pipe (|) in value", "http://url/api"),
Map.of("filter", "a|b|c"),
List.of(new QueryParam("filter", "a|b|c")),
"http://url/api?filter=a%7Cb%7Cc"
),
Arguments.of(
Named.named("caret (^) and backtick (`) in value", "http://url/api"),
Map.of("special", "a^b`c"),
List.of(new QueryParam("special", "a^b`c")),
"http://url/api?special=a%5Eb%60c"
),
Arguments.of(
Named.named("tab character in value", "http://url/api"),
Map.of("data", "col1\tcol2\tcol3"),
List.of(new QueryParam("data", "col1\tcol2\tcol3")),
"http://url/api?data=col1%09col2%09col3"
),
Arguments.of(
Named.named("CRLF in value", "http://url/api"),
Map.of("text", "line1\r\nline2"),
List.of(new QueryParam("text", "line1\r\nline2")),
"http://url/api?text=line1%0D%0Aline2"
),
Arguments.of(
Named.named("square brackets in value", "http://url/api"),
Map.of("array", "[1,2,3]"),
List.of(new QueryParam("array", "[1,2,3]")),
"http://url/api?array=%5B1%2C2%2C3%5D"
),
Arguments.of(
Named.named("single and double quotes in value", "http://url/api"),
Map.of("quoted", "He said \"Hello\" and 'Hi'"),
List.of(new QueryParam("quoted", "He said \"Hello\" and 'Hi'")),
"http://url/api?quoted=He%20said%20%22Hello%22%20and%20%27Hi%27"
),
Arguments.of(
Named.named("angle brackets in value (XSS test)", "http://url/api"),
Map.of("html", "<script>alert('xss')</script>"),
List.of(new QueryParam("html", "<script>alert('xss')</script>")),
"http://url/api?html=%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E"
),
Arguments.of(
Named.named("backslash in value", "http://url/api"),
Map.of("path", "C:\\Users\\test"),
List.of(new QueryParam("path", "C:\\Users\\test")),
"http://url/api?path=C%3A%5CUsers%5Ctest"
),
Arguments.of(
Named.named("at sign (@) in value", "http://url/api"),
Map.of("contact", "user@domain.com"),
List.of(new QueryParam("contact", "user@domain.com")),
"http://url/api?contact=user%40domain.com"
),
Arguments.of(
Named.named("URL as value", "http://url/api"),
Map.of("redirect", "https://example.com/path?foo=bar"),
List.of(new QueryParam("redirect", "https://example.com/path?foo=bar")),
"http://url/api?redirect=https%3A%2F%2Fexample.com%2Fpath%3Ffoo%3Dbar"
),
Arguments.of(
Named.named("empty map (not null)", "http://url/api"),
Collections.emptyMap(),
Named.named("empty list (not null)", "http://url/api"),
Collections.emptyList(),
"http://url/api"
),
Arguments.of(
Named.named("underscore and hyphen in key/value", "http://url/api"),
Map.of("my-key_name", "my-value_data"),
List.of(new QueryParam("my-key_name", "my-value_data")),
"http://url/api?my-key_name=my-value_data"
),
Arguments.of(
Named.named("dots in key and value", "http://url/api"),
Map.of("version.major", "1.2.3"),
List.of(new QueryParam("version.major", "1.2.3")),
"http://url/api?version.major=1.2.3"
),
Arguments.of(
Named.named("multiple reserved chars combined", "http://url/api"),
Map.of("complex", "a=1&b=2#section?query"),
List.of(new QueryParam("complex", "a=1&b=2#section?query")),
"http://url/api?complex=a%3D1%26b%3D2%23section%3Fquery"
),
Arguments.of(
Named.named("trailing slash in URL", "http://url/api/"),
Map.of("param", "value"),
List.of(new QueryParam("param", "value")),
"http://url/api/?param=value"
),
Arguments.of(
Named.named("long value (1000 chars)", "http://url/api"),
Map.of("long", "a".repeat(1000)),
List.of(new QueryParam("long", "a".repeat(1000))),
"http://url/api?long=" + "a".repeat(1000)
),
Arguments.of(
Named.named("Chinese characters in value", "http://url/api"),
Map.of("greeting", "你好世界"),
List.of(new QueryParam("greeting", "你好世界")),
"http://url/api?greeting=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C"
),
Arguments.of(
Named.named("Japanese characters in value", "http://url/api"),
Map.of("text", "こんにちは"),
List.of(new QueryParam("text", "こんにちは")),
"http://url/api?text=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF"
),
Arguments.of(
Named.named("existing query params in URL", "http://url/api?existing=param"),
Map.of("new", "value"),
List.of(new QueryParam("new", "value")),
"http://url/api?existing=param&new=value"
),
Arguments.of(
Named.named("existing query and fragment in URL", "http://url/api?existing=param#section"),
Map.of("additional", "data"),
List.of(new QueryParam("additional", "data")),
"http://url/api?existing=param&additional=data#section"
),
Arguments.of(
Named.named("null byte in value", "http://url/api"),
Map.of("data", "before\u0000after"),
List.of(new QueryParam("data", "before\u0000after")),
"http://url/api?data=before%00after"
),
Arguments.of(
Named.named("form feed and vertical tab in value", "http://url/api"),
Map.of("whitespace", "a\fb\u000Bc"),
List.of(new QueryParam("whitespace", "a\fb\u000Bc")),
"http://url/api?whitespace=a%0Cb%0Bc"
),
Arguments.of(
Named.named("tilde (unreserved, not encoded)", "http://url/api"),
Map.of("pattern", "~user"),
List.of(new QueryParam("pattern", "~user")),
"http://url/api?pattern=~user"
),
Arguments.of(
Named.named("asterisk in value", "http://url/api"),
Map.of("wildcard", "*.txt"),
List.of(new QueryParam("wildcard", "*.txt")),
"http://url/api?wildcard=%2A.txt"
),
Arguments.of(
Named.named("curly braces in key", "http://url/api"),
Map.of("{key}", "value"),
List.of(new QueryParam("{key}", "value")),
"http://url/api?%7Bkey%7D=value"
),
Arguments.of(
Named.named("curly braces in value", "http://url/api"),
Map.of("data", "{value}"),
List.of(new QueryParam("data", "{value}")),
"http://url/api?data=%7Bvalue%7D"
),
Arguments.of(
Named.named("curly braces in both key and value", "http://url/api"),
Map.of("{param}", "{data}"),
List.of(new QueryParam("{param}", "{data}")),
"http://url/api?%7Bparam%7D=%7Bdata%7D"
),
Arguments.of(
Named.named("duplicate param names with different values", "http://url/api"),
List.of(
new QueryParam("test", "a+b"),
new QueryParam("test", "b c")
),
"http://url/api?test=a%2Bb&test=b%20c"
)
);
}

27
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java

@ -52,8 +52,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@ -120,14 +119,28 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
}
}
@Test
public void shouldNotAllowNullQueryParamNames() {
// GIVEN
var config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
config.setUseNewEncoding(true);
config.setQueryParams(List.of(new QueryParam(null, "value")));
// WHEN-THEN
assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
.isInstanceOf(TbNodeException.class)
.matches(e -> ((TbNodeException) e).isUnrecoverable())
.rootCause()
.isInstanceOf(DataValidationException.class)
.hasMessageContaining("query parameter names must be non-null");
}
@Test
public void shouldNotAllowNullQueryParamValues() {
// GIVEN
var config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
config.setUseNewEncoding(true);
var params = new HashMap<String, String>();
params.put("key", null);
config.setQueryParams(params);
config.setQueryParams(List.of(new QueryParam("key", null)));
// WHEN-THEN
assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
@ -143,7 +156,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
// GIVEN
var config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
config.setUseNewEncoding(false);
config.setQueryParams(Map.of("key", "value"));
config.setQueryParams(List.of(new QueryParam("key", "value")));
// WHEN-THEN
assertThatThrownBy(() -> new TbRestApiCallNode().init(ctx, new TbNodeConfiguration(JacksonUtil.valueToTree(config))))
@ -175,7 +188,7 @@ public class TbRestApiCallNodeTest extends AbstractRuleNodeUpgradeTest {
// THEN
assertTrue(defaultConfig.isUseNewEncoding());
assertEquals(Collections.emptyMap(), defaultConfig.getQueryParams());
assertEquals(Collections.emptyList(), defaultConfig.getQueryParams());
}
@Test

Loading…
Cancel
Save