@ -15,39 +15,47 @@
* /
* /
package org.thingsboard.rule.engine.rest ;
package org.thingsboard.rule.engine.rest ;
import io.netty.channel.EventLoopGroup ;
import io.netty.channel.EventLoopGroup ;
import io.netty.channel.nio.NioEventLoopGroup ;
import io.netty.channel.nio.NioEventLoopGroup ;
import org.junit.jupiter.api.AfterEach ;
import org.junit.jupiter.api.AfterEach ;
import org.junit.jupiter.api.Assertions ;
import org.junit.jupiter.api.Assertions ;
import org.junit.jupiter.api.BeforeEach ;
import org.junit.jupiter.api.BeforeEach ;
import org.junit.jupiter.api.Named ;
import org.junit.jupiter.api.Test ;
import org.junit.jupiter.api.Test ;
import org.mockito.ArgumentCaptor ;
import org.junit.jupiter.params.ParameterizedTest ;
import org.junit.jupiter.params.provider.Arguments ;
import org.junit.jupiter.params.provider.MethodSource ;
import org.mockito.Mockito ;
import org.mockito.Mockito ;
import org.mockserver.integration.ClientAndServer ;
import org.springframework.util.LinkedMultiValueMap ;
import org.springframework.util.LinkedMultiValueMap ;
import org.thingsboard.rule.engine.api.TbContext ;
import org.thingsboard.rule.engine.api.TbContext ;
import org.thingsboard.server.common.data.id.DeviceId ;
import org.thingsboard.server.common.data.id.DeviceId ;
import org.thingsboard.server.common.data.id.EntityId ;
import org.thingsboard.server.common.data.id.EntityId ;
import org.thingsboard.server.common.data.msg.TbMsgType ;
import org.thingsboard.server.common.data.msg.TbMsgType ;
import org.thingsboard.server.common.data.util.KeyValueEntry ;
import org.thingsboard.server.common.msg.TbMsg ;
import org.thingsboard.server.common.msg.TbMsg ;
import org.thingsboard.server.common.msg.TbMsgMetaData ;
import org.thingsboard.server.common.msg.TbMsgMetaData ;
import java.net.URI ;
import java.net.URI ;
import java.util.Collections ;
import java.util.List ;
import java.util.List ;
import java.util.Map ;
import java.util.Map ;
import java.util.UUID ;
import java.util.concurrent.CountDownLatch ;
import java.util.concurrent.CountDownLatch ;
import java.util.concurrent.TimeUnit ;
import java.util.concurrent.TimeUnit ;
import java.util.concurrent.atomic.AtomicReference ;
import java.util.stream.Stream ;
import static org.assertj.core.api.Assertions.assertThatThrownBy ;
import static org.assertj.core.api.Assertions.assertThatThrownBy ;
import static org.hamcrest.MatcherAssert.assertThat ;
import static org.hamcrest.MatcherAssert.assertThat ;
import static org.hamcrest.Matchers.instanceOf ;
import static org.hamcrest.Matchers.instanceOf ;
import static org.hamcrest.Matchers.is ;
import static org.hamcrest.Matchers.is ;
import static org.junit.jupiter.api.Assertions.assertNull ;
import static org.junit.jupiter.api.Assertions.assertTrue ;
import static org.mockito.ArgumentMatchers.any ;
import static org.mockito.ArgumentMatchers.any ;
import static org.mockito.ArgumentMatchers.eq ;
import static org.mockito.ArgumentMatchers.eq ;
import static org.mockito.BDDMockito.willCallRealMethod ;
import static org.mockito.BDDMockito.willCallRealMethod ;
import static org.mockito.Mockito.mock ;
import static org.mockito.Mockito.mock ;
import static org.mockito.Mockito.times ;
import static org.mockito.Mockito.never ;
import static org.mockito.Mockito.verify ;
import static org.mockito.Mockito.verify ;
import static org.mockito.Mockito.when ;
import static org.mockito.Mockito.when ;
import static org.mockserver.integration.ClientAndServer.startClientAndServer ;
import static org.mockserver.integration.ClientAndServer.startClientAndServer ;
@ -86,123 +94,461 @@ public class TbHttpClientTest {
@Test
@Test
public void testBuildSimpleUri ( ) {
public void testBuildSimpleUri ( ) {
Mockito . when ( client . buildEncodedUri ( any ( ) ) ) . thenCallRealMethod ( ) ;
Mockito . when ( client . buildEncodedUri ( any ( ) , any ( ) ) ) . thenCallRealMethod ( ) ;
String url = "http://localhost:8080/" ;
String url = "http://localhost:8080/" ;
URI uri = client . buildEncodedUri ( url ) ;
URI uri = client . buildEncodedUri ( url , null ) ;
Assertions . assertEquals ( url , uri . toString ( ) ) ;
Assertions . assertEquals ( url , uri . toString ( ) ) ;
}
}
@Test
@Test
public void testBuildUriWithoutProtocol ( ) {
public void testBuildUriWithoutProtocol ( ) {
Mockito . when ( client . buildEncodedUri ( any ( ) ) ) . thenCallRealMethod ( ) ;
Mockito . when ( client . buildEncodedUri ( any ( ) , any ( ) ) ) . thenCallRealMethod ( ) ;
String url = "localhost:8080/" ;
String url = "localhost:8080/" ;
assertThatThrownBy ( ( ) - > client . buildEncodedUri ( url ) ) ;
assertThatThrownBy ( ( ) - > client . buildEncodedUri ( url , null ) ) ;
}
}
@Test
@Test
public void testBuildInvalidUri ( ) {
public void testBuildInvalidUri ( ) {
Mockito . when ( client . buildEncodedUri ( any ( ) ) ) . thenCallRealMethod ( ) ;
Mockito . when ( client . buildEncodedUri ( any ( ) , any ( ) ) ) . thenCallRealMethod ( ) ;
String url = "aaa" ;
String url = "aaa" ;
assertThatThrownBy ( ( ) - > client . buildEncodedUri ( url ) ) ;
assertThatThrownBy ( ( ) - > client . buildEncodedUri ( url , null ) ) ;
}
}
@Test
@Test
public void testBuildUriWithSpecialSymbols ( ) {
public void testBuildUriWithSpecialSymbols ( ) {
Mockito . when ( client . buildEncodedUri ( any ( ) ) ) . thenCallRealMethod ( ) ;
Mockito . when ( client . buildEncodedUri ( any ( ) , any ( ) ) ) . thenCallRealMethod ( ) ;
String url = "http://192.168.1.1/data?d={\"a\": 12}" ;
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" ;
String expected = "http://192.168.1.1/data?d=%7B%22a%22:%2012%7D" ;
URI uri = client . buildEncodedUri ( url ) ;
URI uri = client . buildEncodedUri ( url , null ) ;
Assertions . assertEquals ( expected , uri . toString ( ) ) ;
Assertions . assertEquals ( expected , uri . toString ( ) ) ;
}
}
@Test
public void testProcessMessageWithQueryParamsPatternProcessing ( ) throws Exception {
// GIVEN
String path = "/api/notify" ;
try ( var server = startClientAndServer ( "localhost" , 0 ) ) {
server . when (
request ( )
. withMethod ( "GET" )
. withPath ( path )
. withQueryStringParameter ( "email" , "user+tag@test.com" )
. withQueryStringParameter ( "device" , "sensor-1" )
. withQueryStringParameter ( "custom-header" , "header-value" )
. withQueryStringParameter ( "temp" , "25.5" )
. withQueryStringParameter ( "location" , "room-1/zone-A" )
) . respond (
response ( ) . withStatusCode ( 200 )
) ;
var config = new TbRestApiCallNodeConfiguration ( ) . defaultConfiguration ( ) ;
config . setRestEndpointUrlPattern ( "http://localhost:" + server . getPort ( ) + path ) ;
config . setRequestMethod ( "GET" ) ;
config . setQueryParams ( List . of (
new KeyValueEntry < > ( "email" , "${userEmail}" ) , // ${} from metadata
new KeyValueEntry < > ( "device" , "${deviceName}" ) , // ${} from metadata
new KeyValueEntry < > ( "${dynamicParam}" , "${dynamicValue}" ) , // ${} in both key and value
new KeyValueEntry < > ( "temp" , "$[temperature]" ) , // $[] from data
new KeyValueEntry < > ( "location" , "$[sensor.location]" ) // $[] from nested data
) ) ;
var metaData = new TbMsgMetaData ( ) ;
metaData . putValue ( "userEmail" , "user+tag@test.com" ) ;
metaData . putValue ( "deviceName" , "sensor-1" ) ;
metaData . putValue ( "dynamicParam" , "custom-header" ) ;
metaData . putValue ( "dynamicValue" , "header-value" ) ;
var msg = TbMsg . newMsg ( )
. type ( TbMsgType . POST_TELEMETRY_REQUEST )
. originator ( new DeviceId ( EntityId . NULL_UUID ) )
. metaData ( metaData )
. data ( "" "
{ "temperature" : 25 . 5 , "sensor" : { "location" : "room-1/zone-A" } }
"" " )
. build ( ) ;
// WHEN-THEN
processMessageAndWait ( config , msg ) ;
}
}
@Test
@Test
public void testProcessMessageWithJsonInUrlVariable ( ) throws Exception {
public void testProcessMessageWithJsonInUrlVariable ( ) throws Exception {
String host = "localhost" ;
// GIVEN
String path = "/api" ;
String path = "/api" ;
String paramKey = "data" ;
String paramValue = "[{\"test\":\"test\"}]" ;
String paramVal = "[{\"test\":\"test\"}]" ;
String successResponseBody = "SUCCESS" ;
try ( var server = startClientAndServer ( "localhost" , 0 ) ) {
server . when (
var server = setUpDummyServer ( host , path , paramKey , paramVal , successResponseBody ) ;
request ( )
. withMethod ( "GET" )
String endpointUrl = String . format (
. withPath ( path )
"http://%s:%d%s?%s=%s" ,
. withQueryStringParameter ( "data" , paramValue )
host , server . getPort ( ) , path , paramKey , paramVal
) . respond (
) ;
response ( ) . withStatusCode ( 200 )
String method = "GET" ;
) ;
var config = new TbRestApiCallNodeConfiguration ( ) . defaultConfiguration ( ) ;
var config = new TbRestApiCallNodeConfiguration ( )
config . setRestEndpointUrlPattern ( "http://localhost:" + server . getPort ( ) + path + "?data=" + paramValue ) ;
. defaultConfiguration ( ) ;
config . setRequestMethod ( "GET" ) ;
config . setRequestMethod ( method ) ;
config . setQueryParams ( null ) ;
config . setRestEndpointUrlPattern ( endpointUrl ) ;
config . setUseSimpleClientHttpFactory ( true ) ;
var msg = TbMsg . newMsg ( )
. type ( TbMsgType . POST_TELEMETRY_REQUEST )
. originator ( new DeviceId ( UUID . randomUUID ( ) ) )
. metaData ( new TbMsgMetaData ( ) )
. data ( TbMsg . EMPTY_JSON_OBJECT )
. build ( ) ;
// WHEN-THEN
processMessageAndWait ( config , msg ) ;
}
}
private void processMessageAndWait ( TbRestApiCallNodeConfiguration config , TbMsg msg ) throws Exception {
var httpClient = new TbHttpClient ( config , eventLoop ) ;
var httpClient = new TbHttpClient ( config , eventLoop ) ;
var msg = TbMsg . newMsg ( )
. type ( TbMsgType . POST_TELEMETRY_REQUEST )
. originator ( new DeviceId ( EntityId . NULL_UUID ) )
. copyMetaData ( TbMsgMetaData . EMPTY )
. data ( TbMsg . EMPTY_JSON_OBJECT )
. build ( ) ;
var successMsg = TbMsg . newMsg ( )
. type ( TbMsgType . POST_TELEMETRY_REQUEST )
. originator ( msg . getOriginator ( ) )
. copyMetaData ( msg . getMetaData ( ) )
. data ( msg . getData ( ) )
. build ( ) ;
var ctx = mock ( TbContext . class ) ;
var ctx = mock ( TbContext . class ) ;
when ( ctx . transformMsg (
when ( ctx . transformMsg ( eq ( msg ) , any ( ) , any ( ) ) ) . thenReturn ( msg ) ;
eq ( msg ) ,
eq ( msg . getMetaData ( ) ) ,
eq ( msg . getData ( ) )
) ) . thenReturn ( successMsg ) ;
var capturedData = ArgumentCaptor . forClass ( String . class ) ;
when ( ctx . transformMsg (
var latch = new CountDownLatch ( 1 ) ;
eq ( msg ) ,
var error = new AtomicReference < Throwable > ( ) ;
any ( ) ,
capturedData . capture ( )
) ) . thenReturn ( successMsg ) ;
CountDownLatch latch = new CountDownLatch ( 1 ) ;
httpClient . processMessage ( ctx , msg ,
httpClient . processMessage ( ctx , msg ,
m - > {
m - > {
ctx . tellSuccess ( msg ) ;
ctx . tellSuccess ( m ) ;
latch . countDown ( ) ;
latch . countDown ( ) ;
} ,
} ,
( m , t ) - > {
( m , t ) - > {
ctx . tellFailure ( m , t ) ;
ctx . tellFailure ( m , t ) ;
error . set ( t ) ;
latch . countDown ( ) ;
latch . countDown ( ) ;
} ) ;
} ) ;
latch . await ( 5 , TimeUnit . SECONDS ) ;
assertTrue ( latch . await ( 5 , TimeUnit . SECONDS ) , "Request should complete within timeout" ) ;
assertNull ( error . get ( ) , "Request should succeed, but got: " + error . get ( ) ) ;
verify ( ctx , times ( 1 ) ) . tellSuccess ( any ( ) ) ;
verify ( ctx ) . tellSuccess ( any ( ) ) ;
verify ( ctx , times ( 0 ) ) . tellFailure ( any ( ) , any ( ) ) ;
verify ( ctx , never ( ) ) . tellFailure ( any ( ) , any ( ) ) ;
Assertions . assertEquals ( successResponseBody , capturedData . getValue ( ) ) ;
}
}
private ClientAndServer setUpDummyServer ( String host , String path , String paramKey , String paramVal , String successResponseBody ) {
var server = startClientAndServer ( host , 1080 ) ;
@ParameterizedTest ( name = "{0}" )
createGetMethodExpectations ( server , path , paramKey , paramVal , successResponseBody ) ;
@MethodSource
return server ;
public void testQueryParamsEncoding ( String endpointUrl , List < KeyValueEntry < String , String > > queryParams , String expectedEncodedUrl ) {
// GIVEN
Mockito . when ( client . buildEncodedUri ( any ( ) , any ( ) ) ) . thenCallRealMethod ( ) ;
// WHEN
URI uri = client . buildEncodedUri ( endpointUrl , queryParams ) ;
// THEN
Assertions . assertEquals ( expectedEncodedUrl , uri . toASCIIString ( ) ) ;
}
}
private void createGetMethodExpectations ( ClientAndServer server , String path , String paramKey , String paramVal , String successResponseBody ) {
private static Stream < Arguments > testQueryParamsEncoding ( ) {
server . when (
return Stream . of (
request ( )
Arguments . of (
. withMethod ( "GET" )
Named . named ( "ISO 8601 date-time in value" , "http://somecompany/api/data/fetch" ) ,
. withPath ( path )
List . of ( new KeyValueEntry < > ( "ts" , "2016-08-01T09:06:06.0+02:00" ) ) ,
. withQueryStringParameter ( paramKey , paramVal )
"http://somecompany/api/data/fetch?ts=2016-08-01T09%3A06%3A06.0%2B02%3A00"
) . respond (
) ,
response ( )
Arguments . of (
. withStatusCode ( 200 )
Named . named ( "email with plus sign in value" , "http://localhost:8080/api/user/sendActivationMail" ) ,
. withBody ( successResponseBody )
List . of ( new KeyValueEntry < > ( "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" ) ,
List . of ( new KeyValueEntry < > ( "q" , "a + b" ) ) ,
"http://url/api?q=a%20%2B%20b"
) ,
Arguments . of (
Named . named ( "colon in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "time" , "12:00" ) ) ,
"http://url/api?time=12%3A00"
) ,
Arguments . of (
Named . named ( "slash in value" , "http://url" ) ,
List . of ( new KeyValueEntry < > ( "ref" , "/home/user" ) ) ,
"http://url?ref=%2Fhome%2Fuser"
) ,
Arguments . of (
Named . named ( "comma and semicolon in value" , "http://url" ) ,
List . of ( new KeyValueEntry < > ( "l" , "a,b;c" ) ) ,
"http://url?l=a%2Cb%3Bc"
) ,
Arguments . of (
Named . named ( "ampersand and equals in value" , "http://url" ) ,
List . of ( new KeyValueEntry < > ( "q" , "key1=value1&key2=value2" ) ) ,
"http://url?q=key1%3Dvalue1%26key2%3Dvalue2"
) ,
Arguments . of (
Named . named ( "JSON in value" , "http://url" ) ,
List . of ( new KeyValueEntry < > ( "json" , "" "
{
"string" : "hello" ,
"integer" : 42 ,
"float" : 3 . 14 ,
"boolTrue" : true ,
"boolFalse" : false ,
"null" : null ,
"array" : [
1 ,
"two" ,
true ,
null
] ,
"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" ) ,
List . of (
new KeyValueEntry < > ( "nom" , "Le Goût Moderne" ) ,
new KeyValueEntry < > ( "назва" , "У Миколи \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" ) ,
List . of ( new KeyValueEntry < > ( "name" , "" ) ) ,
"http://url/empty?name="
) ,
Arguments . of (
Named . named ( "blank value (spaces)" , "http://url/empty" ) ,
List . of ( new KeyValueEntry < > ( "name" , " " ) ) ,
"http://url/empty?name=%20%20"
) ,
Arguments . of (
Named . named ( "empty key" , "http://url/empty" ) ,
List . of ( new KeyValueEntry < > ( "" , "value" ) ) ,
"http://url/empty?=value"
) ,
Arguments . of (
Named . named ( "blank key (spaces)" , "http://url/empty" ) ,
List . of ( new KeyValueEntry < > ( " " , "value" ) ) ,
"http://url/empty?%20%20=value"
) ,
Arguments . of (
Named . named ( "blank key with value that needs to be encoded" , "http://url/empty" ) ,
List . of ( new KeyValueEntry < > ( " " , "value1+value2" ) ) ,
"http://url/empty?%20%20=value1%2Bvalue2"
) ,
Arguments . of (
Named . named ( "blank key and value" , "http://url/empty" ) ,
List . of ( new KeyValueEntry < > ( " " , " " ) ) ,
"http://url/empty?%20%20=%20%20"
) ,
Arguments . of (
Named . named ( "fragment with query params" , "http://url#frag" ) ,
List . of ( new KeyValueEntry < > ( "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 . emptyList ( ) ,
"http://url#frag"
) ,
Arguments . of (
Named . named ( "multiple query params (ordered)" , "http://url/api" ) ,
List . of (
new KeyValueEntry < > ( "param1" , "value1" ) ,
new KeyValueEntry < > ( "param2" , "value2" ) ,
new KeyValueEntry < > ( "param3" , "value3" )
) ,
"http://url/api?param1=value1¶m2=value2¶m3=value3"
) ,
Arguments . of (
Named . named ( "hash (#) in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "color" , "#ff0000" ) ) ,
"http://url/api?color=%23ff0000"
) ,
Arguments . of (
Named . named ( "question mark (?) in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "query" , "what?" ) ) ,
"http://url/api?query=what%3F"
) ,
Arguments . of (
Named . named ( "percent sign (%) in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "discount" , "50%" ) ) ,
"http://url/api?discount=50%25"
) ,
Arguments . of (
Named . named ( "already encoded string - double encoding" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "encoded" , "%20" ) ) ,
"http://url/api?encoded=%2520"
) ,
Arguments . of (
Named . named ( "URL with port number" , "http://localhost:8080/api/v1/data" ) ,
List . of ( new KeyValueEntry < > ( "key" , "value" ) ) ,
"http://localhost:8080/api/v1/data?key=value"
) ,
Arguments . of (
Named . named ( "URL with userinfo (auth)" , "http://user:password@hostname/path" ) ,
List . of ( new KeyValueEntry < > ( "secure" , "true" ) ) ,
"http://user:password@hostname/path?secure=true"
) ,
Arguments . of (
Named . named ( "IPv4 address in URL" , "http://192.168.1.100:9090/endpoint" ) ,
List . of ( new KeyValueEntry < > ( "ip" , "test" ) ) ,
"http://192.168.1.100:9090/endpoint?ip=test"
) ,
Arguments . of (
Named . named ( "IPv6 address in URL" , "http://[::1]:8080/api" ) ,
List . of ( new KeyValueEntry < > ( "ipv6" , "true" ) ) ,
"http://[::1]:8080/api?ipv6=true"
) ,
Arguments . of (
Named . named ( "HTTPS protocol" , "https://secure.example.com/api" ) ,
List . of ( new KeyValueEntry < > ( "token" , "abc123" ) ) ,
"https://secure.example.com/api?token=abc123"
) ,
Arguments . of (
Named . named ( "pipe (|) in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "filter" , "a|b|c" ) ) ,
"http://url/api?filter=a%7Cb%7Cc"
) ,
Arguments . of (
Named . named ( "caret (^) and backtick (`) in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "special" , "a^b`c" ) ) ,
"http://url/api?special=a%5Eb%60c"
) ,
Arguments . of (
Named . named ( "tab character in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "data" , "col1\tcol2\tcol3" ) ) ,
"http://url/api?data=col1%09col2%09col3"
) ,
Arguments . of (
Named . named ( "CRLF in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "text" , "line1\r\nline2" ) ) ,
"http://url/api?text=line1%0D%0Aline2"
) ,
Arguments . of (
Named . named ( "square brackets in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "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" ) ,
List . of ( new KeyValueEntry < > ( "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" ) ,
List . of ( new KeyValueEntry < > ( "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" ) ,
List . of ( new KeyValueEntry < > ( "path" , "C:\\Users\\test" ) ) ,
"http://url/api?path=C%3A%5CUsers%5Ctest"
) ,
Arguments . of (
Named . named ( "at sign (@) in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "contact" , "user@domain.com" ) ) ,
"http://url/api?contact=user%40domain.com"
) ,
Arguments . of (
Named . named ( "URL as value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "redirect" , "https://example.com/path?foo=bar" ) ) ,
"http://url/api?redirect=https%3A%2F%2Fexample.com%2Fpath%3Ffoo%3Dbar"
) ,
Arguments . of (
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" ) ,
List . of ( new KeyValueEntry < > ( "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" ) ,
List . of ( new KeyValueEntry < > ( "version.major" , "1.2.3" ) ) ,
"http://url/api?version.major=1.2.3"
) ,
Arguments . of (
Named . named ( "multiple reserved chars combined" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "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/" ) ,
List . of ( new KeyValueEntry < > ( "param" , "value" ) ) ,
"http://url/api/?param=value"
) ,
Arguments . of (
Named . named ( "long value (1000 chars)" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "long" , "a" . repeat ( 1000 ) ) ) ,
"http://url/api?long=" + "a" . repeat ( 1000 )
) ,
Arguments . of (
Named . named ( "Chinese characters in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "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" ) ,
List . of ( new KeyValueEntry < > ( "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" ) ,
List . of ( new KeyValueEntry < > ( "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" ) ,
List . of ( new KeyValueEntry < > ( "additional" , "data" ) ) ,
"http://url/api?existing=param&additional=data#section"
) ,
Arguments . of (
Named . named ( "null byte in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "data" , "before\u0000after" ) ) ,
"http://url/api?data=before%00after"
) ,
Arguments . of (
Named . named ( "form feed and vertical tab in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "whitespace" , "a\fb\u000Bc" ) ) ,
"http://url/api?whitespace=a%0Cb%0Bc"
) ,
Arguments . of (
Named . named ( "tilde (unreserved, not encoded)" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "pattern" , "~user" ) ) ,
"http://url/api?pattern=~user"
) ,
Arguments . of (
Named . named ( "asterisk in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "wildcard" , "*.txt" ) ) ,
"http://url/api?wildcard=%2A.txt"
) ,
Arguments . of (
Named . named ( "curly braces in key" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "{key}" , "value" ) ) ,
"http://url/api?%7Bkey%7D=value"
) ,
Arguments . of (
Named . named ( "curly braces in value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "data" , "{value}" ) ) ,
"http://url/api?data=%7Bvalue%7D"
) ,
Arguments . of (
Named . named ( "curly braces in both key and value" , "http://url/api" ) ,
List . of ( new KeyValueEntry < > ( "{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 KeyValueEntry < > ( "test" , "a+b" ) ,
new KeyValueEntry < > ( "test" , "b c" )
) ,
"http://url/api?test=a%2Bb&test=b%20c"
)
) ;
) ;
}
}