diff --git a/README.md b/README.md index 6f64d4c1d..e3a9d62f6 100755 --- a/README.md +++ b/README.md @@ -1589,17 +1589,23 @@ Given url 'https://' + e2eHostName + '/v1/api' If you are trying to build dynamic URLs including query-string parameters in the form: `http://myhost/some/path?foo=bar&search=true` - please refer to the [`param`](#param) keyword. ## `path` -REST-style path parameters. Can be expressions that will be evaluated. Comma delimited values are supported which can be more convenient, and takes care of URL-encoding and appending '/' where needed. +REST-style path parameters. Can be expressions that will be evaluated. Comma delimited values are supported which can be more convenient, and takes care of URL-encoding and appending '/' between path segments as needed. ```cucumber +# this is invalid and will result in / being encoded as %2F when sent to the remote server +# eg. given a documentId of 1234 the path will be: /documents%2F1234%2Fdownload Given path 'documents/' + documentId + '/download' -# this is equivalent to the above +# this is the correct way to specify multiple paths Given path 'documents', documentId, 'download' # or you can do the same on multiple lines if you wish Given path 'documents' And path documentId And path 'download' + +# you can also ensure that the constructed url has a trailing / by appending an empty path segment +# eg. given a documentId of 1234 the path will be: /documents/1234/download/ +Given path 'documents', documentId, 'download', '' ``` Note that the `path` 'resets' after any HTTP request is made but not the `url`. The [Hello World](#hello-world) is a great example of 'REST-ful' use of the `url` when the test focuses on a single REST 'resource'. Look at how the `path` did not need to be specified for the second HTTP `get` call since `/cats` is part of the `url`. @@ -1820,7 +1826,7 @@ Use this for multipart content items that don't have field-names. Here below is also demonstrates using the [`multipart/related`](https://tools.ietf.org/html/rfc2387) content-type. ```cucumber -Given path '/v2/documents' +Given path 'v2', 'documents' And multipart entity read('foo.json') And multipart field image = read('bar.jpg') And header Content-Type = 'multipart/related' diff --git a/karate-core/src/main/java/com/intuit/karate/http/HttpClientFactory.java b/karate-core/src/main/java/com/intuit/karate/http/HttpClientFactory.java index c22bc2c5a..09a900d3e 100644 --- a/karate-core/src/main/java/com/intuit/karate/http/HttpClientFactory.java +++ b/karate-core/src/main/java/com/intuit/karate/http/HttpClientFactory.java @@ -34,6 +34,6 @@ public interface HttpClientFactory { HttpClient create(ScenarioEngine engine); - public static final HttpClientFactory DEFAULT = engine -> new ApacheHttpClient(engine); + HttpClientFactory DEFAULT = ApacheHttpClient::new; } diff --git a/karate-core/src/main/java/com/intuit/karate/http/HttpRequestBuilder.java b/karate-core/src/main/java/com/intuit/karate/http/HttpRequestBuilder.java index 079616f89..6a9697f89 100644 --- a/karate-core/src/main/java/com/intuit/karate/http/HttpRequestBuilder.java +++ b/karate-core/src/main/java/com/intuit/karate/http/HttpRequestBuilder.java @@ -28,11 +28,11 @@ import com.intuit.karate.graal.JsArray; import com.intuit.karate.graal.JsValue; import com.intuit.karate.graal.Methods; -import com.linecorp.armeria.common.QueryParams; -import com.linecorp.armeria.common.QueryParamsBuilder; import io.netty.handler.codec.http.cookie.ClientCookieEncoder; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.DefaultCookie; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; @@ -47,7 +47,9 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.http.client.utils.URIBuilder; import org.graalvm.polyglot.Value; import org.graalvm.polyglot.proxy.ProxyObject; import org.slf4j.Logger; @@ -84,7 +86,7 @@ public class HttpRequestBuilder implements ProxyObject { URL, METHOD, PATH, PARAM, PARAMS, HEADER, HEADERS, BODY, INVOKE, GET, POST, PUT, DELETE, PATCH, HEAD, CONNECT, OPTIONS, TRACE }; - private static final Set KEY_SET = new HashSet(Arrays.asList(KEYS)); + private static final Set KEY_SET = new HashSet<>(Arrays.asList(KEYS)); private static final JsArray KEY_ARRAY = new JsArray(KEYS); private String url; @@ -159,14 +161,7 @@ public HttpRequest build() { } multiPart = null; } - String urlAndPath = getUrlAndPath(); - if (params != null) { - QueryParamsBuilder qpb = QueryParams.builder(); - params.forEach((k, v) -> qpb.add(k, v)); - String append = urlAndPath.indexOf('?') == -1 ? "?" : "&"; - urlAndPath = urlAndPath + append + qpb.toQueryString(); - } - request.setUrl(urlAndPath); + request.setUrl(getUri().toASCIIString()); if (multiPart != null) { if (body == null) { // this is not-null only for a re-try, don't rebuild multi-part body = multiPart.build(); @@ -183,7 +178,7 @@ public HttpRequest build() { request.setBodyForDisplay(multiPart.getBodyForDisplay()); } if (cookies != null && !cookies.isEmpty()) { - List cookieValues = new ArrayList(cookies.size()); + List cookieValues = new ArrayList<>(cookies.size()); for (Cookie c : cookies) { String cookieValue = ClientCookieEncoder.LAX.encode(c); cookieValues.add(cookieValue); @@ -263,32 +258,29 @@ public HttpRequestBuilder path(String path) { return this; } - private String getPath() { - String temp = ""; - if (paths == null) { - return temp; - } - for (String path : paths) { - if (path.startsWith("/")) { - path = path.substring(1); + private URI getUri() { + try { + URIBuilder builder = url == null ? new URIBuilder() : new URIBuilder(url); + if (params != null) { + params.forEach((key, values) -> values.forEach(value -> builder.addParameter(key, value))); } - if (!temp.isEmpty() && !temp.endsWith("/")) { - temp = temp + "/"; + if (paths != null) { + paths.forEach(path -> { + if (path.startsWith("/")) { + logger.warn("Path segment: '{}' starts with a '/', this is probably a mistake. The '/' character will be escaped and sent to the remote server as '%2F'. " + + "If you want to include multiple paths please separate them using commas. Ie. 'hello', 'world' instead of '/hello/world'.", path); + } + }); + // merge paths from the supplied url with additional paths supplied to this builder + List merged = Stream.of(builder.getPathSegments(), paths) + .flatMap(List::stream) + .collect(Collectors.toList()); + builder.setPathSegments(merged); } - temp = temp + path; - } - return temp; - } - - public String getUrlAndPath() { - if (url == null) { - url = ""; - } - String path = getPath(); - if (path.isEmpty()) { - return url; + return builder.build(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); } - return url.endsWith("/") ? url + path : url + "/" + path; } public HttpRequestBuilder body(Object body) { @@ -331,7 +323,7 @@ public HttpRequestBuilder header(String name, String... values) { public HttpRequestBuilder header(String name, List values) { if (headers == null) { - headers = new LinkedHashMap(); + headers = new LinkedHashMap<>(); } for (String key : headers.keySet()) { if (key.equalsIgnoreCase(name)) { @@ -390,7 +382,7 @@ public HttpRequestBuilder param(String name, String... values) { public HttpRequestBuilder param(String name, List values) { if (params == null) { - params = new HashMap(); + params = new HashMap<>(); } List notNullValues = values.stream().filter(v -> v != null).collect(Collectors.toList()); if (!notNullValues.isEmpty()) { @@ -417,7 +409,7 @@ public HttpRequestBuilder cookie(Map map) { public HttpRequestBuilder cookie(Cookie cookie) { if (cookies == null) { - cookies = new HashSet(); + cookies = new HashSet<>(); } cookies.add(cookie); return this; @@ -451,7 +443,7 @@ public HttpRequestBuilder multiPart(Map map) { // private final Methods.FunVar PATH_FUNCTION = args -> { if (args.length == 0) { - return getPath(); + return getUri().getPath(); } else { for (Object o : args) { if (o != null) { @@ -596,7 +588,7 @@ public boolean hasMember(String key) { @Override public String toString() { - return getUrlAndPath(); + return getUri().toASCIIString(); } } diff --git a/karate-core/src/main/java/com/intuit/karate/http/HttpUtils.java b/karate-core/src/main/java/com/intuit/karate/http/HttpUtils.java index ebea92fed..6e0fac5a8 100644 --- a/karate-core/src/main/java/com/intuit/karate/http/HttpUtils.java +++ b/karate-core/src/main/java/com/intuit/karate/http/HttpUtils.java @@ -64,7 +64,7 @@ public static Map parseContentTypeParams(String mimeType) { if (count <= 1) { return null; } - Map map = new LinkedHashMap(count - 1); + Map map = new LinkedHashMap<>(count - 1); for (int i = 1; i < count; i++) { String item = items.get(i); int pos = item.indexOf('='); @@ -90,7 +90,7 @@ public static Map parseUriPattern(String pattern, String url) { if (rightSize != leftSize) { return null; } - Map map = new LinkedHashMap(leftSize); + Map map = new LinkedHashMap<>(leftSize); for (int i = 0; i < leftSize; i++) { String left = leftList.get(i); String right = rightList.get(i); @@ -107,7 +107,7 @@ public static Map parseUriPattern(String pattern, String url) { return map; } - public static final String normaliseUriPath(String uri) { + public static String normaliseUriPath(String uri) { uri = uri.indexOf('?') == -1 ? uri : uri.substring(0, uri.indexOf('?')); if (uri.endsWith("/")) { uri = uri.substring(0, uri.length() - 1); @@ -224,7 +224,7 @@ public static FullHttpResponse transform(FullHttpResponse original, String body) private static final HttpResponseStatus CONNECTION_ESTABLISHED = new HttpResponseStatus(200, "Connection established"); - public static final FullHttpResponse connectionEstablished() { + public static FullHttpResponse connectionEstablished() { return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, CONNECTION_ESTABLISHED); } @@ -243,7 +243,7 @@ public static void addViaHeader(HttpMessage msg, String alias) { List list; if (msg.headers().contains(HttpHeaderNames.VIA)) { List existing = msg.headers().getAll(HttpHeaderNames.VIA); - list = new ArrayList(existing); + list = new ArrayList<>(existing); list.add(sb.toString()); } else { list = Collections.singletonList(sb.toString()); diff --git a/karate-core/src/main/java/com/intuit/karate/http/Request.java b/karate-core/src/main/java/com/intuit/karate/http/Request.java index 3830bd58c..6a9dd88e5 100644 --- a/karate-core/src/main/java/com/intuit/karate/http/Request.java +++ b/karate-core/src/main/java/com/intuit/karate/http/Request.java @@ -92,7 +92,7 @@ public class Request implements ProxyObject { PATH, METHOD, PARAM, PARAMS, HEADER, HEADERS, PATH_PARAM, PATH_PARAMS, BODY, MULTI_PART, MULTI_PARTS, JSON, AJAX, GET, POST, PUT, DELETE, PATCH, HEAD, CONNECT, OPTIONS, TRACE }; - private static final Set KEY_SET = new HashSet(Arrays.asList(KEYS)); + private static final Set KEY_SET = new HashSet<>(Arrays.asList(KEYS)); private static final JsArray KEY_ARRAY = new JsArray(KEYS); private String urlAndPath; @@ -106,7 +106,7 @@ public class Request implements ProxyObject { private ResourceType resourceType; private String resourcePath; private String pathParam; - private List pathParams = Collections.EMPTY_LIST; + private List pathParams = Collections.emptyList(); private RequestContext requestContext; public RequestContext getRequestContext() { @@ -149,7 +149,7 @@ public String getContentType() { public List getCookies() { List cookieValues = getHeaderValues(HttpConstants.HDR_COOKIE); if (cookieValues == null) { - return Collections.EMPTY_LIST; + return Collections.emptyList(); } return cookieValues.stream().map(ClientCookieDecoder.STRICT::decode).collect(toList()); } @@ -178,17 +178,8 @@ public void setUrl(String url) { StringUtils.Pair pair = HttpUtils.parseUriIntoUrlBaseAndPath(url); urlBase = pair.left; QueryStringDecoder qsd = new QueryStringDecoder(pair.right); - String path = qsd.path(); - Map> queryParams = qsd.parameters(); - if (queryParams.size() == 1) { - List list = queryParams.values().iterator().next(); - if (!list.isEmpty() && "".equals(list.get(0))) { - // annoying edge case where url had encoded characters - path = pair.right.replace('?', '�'); - } - } - setPath(path); - setParams(queryParams); + setPath(qsd.path()); + setParams(qsd.parameters()); } public String getUrlAndPath() { @@ -231,7 +222,7 @@ public void setMethod(String method) { } public Map> getParams() { - return params == null ? Collections.EMPTY_MAP : params; + return params == null ? Collections.emptyMap() : params; } public void setParams(Map> params) { @@ -255,7 +246,7 @@ public void setPathParams(List pathParams) { } public Map> getHeaders() { - return headers == null ? Collections.EMPTY_MAP : headers; + return headers == null ? Collections.emptyMap() : headers; } public void setHeaders(Map> headers) { @@ -282,7 +273,7 @@ public Object getBodyConverted() { try { return JsValue.fromBytes(body, false, rt); } catch (Exception e) { - logger.trace("failed to auto-convert response: {}", e); + logger.trace("failed to auto-convert response", e); return getBodyAsString(); } } @@ -335,14 +326,14 @@ public void processBody() { boolean multipart; if (contentType.startsWith("multipart")) { multipart = true; - multiParts = new HashMap(); + multiParts = new HashMap<>(); } else if (contentType.contains("form-urlencoded")) { multipart = false; } else { return; } logger.trace("decoding content-type: {}", contentType); - params = (params == null || params.isEmpty()) ? new HashMap() : new HashMap(params); // since it may be immutable + params = (params == null || params.isEmpty()) ? new HashMap<>() : new HashMap<>(params); // since it may be immutable DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), path, Unpooled.wrappedBuffer(body)); request.headers().add(HttpConstants.HDR_CONTENT_TYPE, contentType); InterfaceHttpPostRequestDecoder decoder = multipart ? new HttpPostMultipartRequestDecoder(request) : new HttpPostStandardRequestDecoder(request); @@ -350,12 +341,8 @@ public void processBody() { for (InterfaceHttpData part : decoder.getBodyHttpDatas()) { String name = part.getName(); if (multipart && part instanceof FileUpload) { - List> list = multiParts.get(name); - if (list == null) { - list = new ArrayList(); - multiParts.put(name, list); - } - Map map = new HashMap(); + List> list = multiParts.computeIfAbsent(name, k -> new ArrayList<>()); + Map map = new HashMap<>(); list.add(map); FileUpload fup = (FileUpload) part; map.put("name", name); @@ -373,11 +360,7 @@ public void processBody() { } } else { // form-field, url-encoded if not multipart Attribute attribute = (Attribute) part; - List list = params.get(name); - if (list == null) { - list = new ArrayList(); - params.put(name, list); - } + List list = params.computeIfAbsent(name, k -> new ArrayList<>()); list.add(attribute.getValue()); } } diff --git a/karate-core/src/test/java/com/intuit/karate/core/KarateHttpMockHandlerTest.java b/karate-core/src/test/java/com/intuit/karate/core/KarateHttpMockHandlerTest.java index 108e71137..2d6556218 100644 --- a/karate-core/src/test/java/com/intuit/karate/core/KarateHttpMockHandlerTest.java +++ b/karate-core/src/test/java/com/intuit/karate/core/KarateHttpMockHandlerTest.java @@ -68,7 +68,7 @@ void testSimpleGet() { startMockServer(); run( urlStep(), - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "hello world"); @@ -82,7 +82,7 @@ void testThatCookieIsPartOfRequest() { startMockServer(); run( urlStep(), - "path '/hello'", + "path 'hello'", "cookie foo = 'bar'", "method get" ); @@ -97,7 +97,7 @@ void testSameSiteSecureCookieRequest() { startMockServer(); run( urlStep(), - "path '/hello'", + "path 'hello'", "cookie foo = { value: 'bar', samesite: 'Strict', secure: true }", "method get" ); @@ -112,7 +112,7 @@ void testSameSiteSecureCookieResponse() { startMockServer(); run( urlStep(), - "path '/hello'", + "path 'hello'", "method get" ); matchVarContains("responseHeaders", "{ set-cookie: ['foo=bar; expires=Wed, 30-Dec-20 09:25:45 GMT; path=/; domain=.example.com; HttpOnly; SameSite=Lax; Secure'] }"); @@ -126,7 +126,7 @@ void testThatExoticContentTypeIsPreserved() { startMockServer(); run( urlStep(), - "path '/hello'", + "path 'hello'", "header Content-Type = 'application/xxx.pingixxxxxx.checkUsernamePassword+json'", "method post" ); @@ -142,7 +142,7 @@ void testInspectRequestInHeadersFunction() { run( urlStep(), "configure headers = function(request){ return { 'api-key': request.bodyAsString } }", - "path '/hello'", + "path 'hello'", "request 'some text'", "method post" ); @@ -159,7 +159,7 @@ void testKarateRemove() { startMockServer(); run( urlStep(), - "path '/hello/1'", + "path 'hello', '1'", "method get" ); matchVarContains("response", "{ '2': 'bar' }"); @@ -173,7 +173,7 @@ void testTransferEncoding() { startMockServer(); run( urlStep(), - "path '/hello'", + "path 'hello'", "header Transfer-Encoding = 'chunked'", "request { foo: 'bar' }", "method post" @@ -189,7 +189,7 @@ void testMalformedMockResponse() { startMockServer(); run( urlStep(), - "path '/hello'", + "path 'hello'", "method get", "match response == '{ \"id\" \"123\" }'", "match responseType == 'string'" @@ -210,7 +210,7 @@ void testRedirectAfterPostWithCookie() { startMockServer(); run( urlStep(), - "path '/first'", + "path 'first'", "form fields { username: 'blah', password: 'blah' }", "method post" ); diff --git a/karate-core/src/test/java/com/intuit/karate/core/KarateMockHandlerTest.java b/karate-core/src/test/java/com/intuit/karate/core/KarateMockHandlerTest.java index 55ec32084..a2fcc34a9 100644 --- a/karate-core/src/test/java/com/intuit/karate/core/KarateMockHandlerTest.java +++ b/karate-core/src/test/java/com/intuit/karate/core/KarateMockHandlerTest.java @@ -52,7 +52,7 @@ void testSimpleGet() { "def response = 'hello world'"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "hello world"); @@ -65,7 +65,7 @@ void testSimplePost() { "def response = requestHeaders"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "request { foo: 'bar' }", "method post" ); @@ -94,7 +94,7 @@ void testParam() { run( URL_STEP, "param foo = 'bar'", - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "{ foo: ['bar'] }"); @@ -108,7 +108,7 @@ void testParams() { run( URL_STEP, "params { foo: 'bar' }", - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "{ foo: ['bar'] }"); @@ -122,7 +122,7 @@ void testParamWithEmbeddedCommas() { run( URL_STEP, "param foo = 'bar,baz'", - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "{ foo: ['bar,baz'] }"); @@ -136,7 +136,7 @@ void testParamMultiValue() { run( URL_STEP, "param foo = ['bar', 'baz']", - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "{ foo: ['bar', 'baz'] }"); @@ -149,7 +149,7 @@ void testHeaders() { "def response = requestHeaders"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "header foo = 'bar'", "method get" ); @@ -163,7 +163,7 @@ void testHeaderMultiValue() { "def response = requestHeaders"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "def fun = function(arg){ return [arg.first, arg.second] }", "header Authorization = call fun { first: 'foo', second: 'bar' }", "method get" @@ -178,7 +178,7 @@ void testRequestContentTypeForJson() { "def response = requestHeaders"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "request { foo: 'bar' }", "method post" ); @@ -193,7 +193,7 @@ void testResponseContentTypeForJson() { "def response = '{ \"foo\": \"bar\"}'"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "method get", "match responseHeaders == { 'Content-Type': ['application/json'] }", "match header content-type == 'application/json'", @@ -209,7 +209,7 @@ void testCookie() { run( URL_STEP, "cookie foo = 'bar'", - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "{ Cookie: ['foo=bar'] }"); @@ -226,7 +226,7 @@ void testCookieWithDateInThePast() { run( URL_STEP, "cookie foo = {value:'bar', expires: '" + pastDate + "'}", - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "{ Cookie: ['foo=bar'] }"); @@ -243,7 +243,7 @@ void testCookieWithDateInTheFuture() { run( URL_STEP, "cookie foo = { value: 'bar', expires: '" + futureDate + "' }", - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "{ Cookie: ['foo=bar'] }"); @@ -257,7 +257,7 @@ void testCookieWithMaxAgeZero() { run( URL_STEP, "cookie foo = { value: 'bar', max-age: '0' }", - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "{ Cookie: ['#string'] }"); @@ -271,7 +271,7 @@ void testFormFieldGet() { run( URL_STEP, "form field foo = 'bar'", - "path '/hello'", + "path 'hello'", "method get" ); matchVar("response", "{ foo: ['bar'] }"); @@ -285,7 +285,7 @@ void testFormFieldPost() { run( URL_STEP, "form field foo = 'bar'", - "path '/hello'", + "path 'hello'", "method post" ); matchVar("response", "foo=bar"); @@ -299,7 +299,7 @@ void testMultiPartField() { run( URL_STEP, "multipart field foo = 'bar'", - "path '/hello'", + "path 'hello'", "method post" ); matchVar("response", "{ foo: ['bar'] }"); @@ -313,7 +313,7 @@ void testMultiPartFile() { run( URL_STEP, "multipart file foo = { filename: 'foo.txt', value: 'hello' }", - "path '/hello'", + "path 'hello'", "method post" ); matchVar("response", "{ foo: [{ name: 'foo', value: '#notnull', contentType: 'text/plain', charset: 'UTF-8', filename: 'foo.txt', transferEncoding: '7bit' }] }"); @@ -327,7 +327,7 @@ void testConfigureResponseHeaders() { "def response = ''"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "method get" ); matchVar("responseHeaders", "{ 'Content-Type': ['text/html'] }"); @@ -342,7 +342,7 @@ void testConfigureLowerCaseResponseHeaders() { run( "configure lowerCaseResponseHeaders = true", URL_STEP, - "path '/hello'", + "path 'hello'", "method get" ); matchVar("responseHeaders", "{ 'content-type': ['text/html'] }"); @@ -356,7 +356,7 @@ void testResponseContentTypeForXml() { "def response = 'world'"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "method get", "match header content-type == 'application/xml'", "match responseType == 'xml'", @@ -371,7 +371,7 @@ void testResponseAutoConversionForXmlAsPlainText() { "def response = 'world'"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "method get", "match header content-type == 'text/plain'", "match responseType == 'xml'", @@ -386,7 +386,7 @@ void testResponseAutoConversionForJsonAsPlainText() { "def response = '{ \"foo\": \"bar\"}'"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "method get", "match header content-type == 'text/plain'", "match responseType == 'json'", @@ -401,7 +401,7 @@ void testResponseAutoConversionForTextWithTags() { "def response = ' a .'"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "method get", "match header content-type == 'text/plain'", "match responseType == 'string'", @@ -416,7 +416,7 @@ void testResponseContentTypeForNonXmlWithTags() { "def response = ' a .'"); run( URL_STEP, - "path '/hello'", + "path 'hello'", "method get", "match header content-type == 'text/turtle'", "match responseType == 'string'", @@ -431,7 +431,7 @@ void testWildcardLikePathMatch() { "def response = requestUri"); run( URL_STEP, - "path '/hello/foo/bar'", + "path 'hello', 'foo', 'bar'", "method get", "match response == 'hello/foo/bar'" ); diff --git a/karate-core/src/test/java/com/intuit/karate/http/RequestHandlerTest.java b/karate-core/src/test/java/com/intuit/karate/http/RequestHandlerTest.java index 8d62720bf..5e9ecfbb5 100644 --- a/karate-core/src/test/java/com/intuit/karate/http/RequestHandlerTest.java +++ b/karate-core/src/test/java/com/intuit/karate/http/RequestHandlerTest.java @@ -52,7 +52,7 @@ private void matchHeaderContains(String name, String expected) { @Test void testIndexAndAjaxPost() { - request.path("/index"); + request.path("index"); handle(); matchHeaderContains("Set-Cookie", "karate.sid"); matchHeaderEquals("Content-Type", "text/html"); @@ -61,7 +61,7 @@ void testIndexAndAjaxPost() { assertTrue(body.contains("Apple")); assertTrue(body.contains("Orange")); assertTrue(body.contains("Billie")); - request.path("/person") + request.path("person") .contentType("application/x-www-form-urlencoded") .header("HX-Request", "true") .body("firstName=John&lastName=Smith&email=john%40smith.com") diff --git a/karate-demo/pom.xml b/karate-demo/pom.xml index 54d6049be..45a7d4255 100755 --- a/karate-demo/pom.xml +++ b/karate-demo/pom.xml @@ -18,14 +18,19 @@ - - - org.springframework.boot - spring-boot-dependencies - ${spring.boot.version} - pom - import - + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + org.springframework.boot spring-boot-starter-websocket @@ -175,7 +180,7 @@ demo/DemoTestParallel.java - ${argLine} + -Dfile.encoding=UTF-8 ${argLine} diff --git a/karate-demo/src/main/java/com/intuit/karate/demo/controller/EncodingController.java b/karate-demo/src/main/java/com/intuit/karate/demo/controller/EncodingController.java index 116799f5f..01ce5790d 100644 --- a/karate-demo/src/main/java/com/intuit/karate/demo/controller/EncodingController.java +++ b/karate-demo/src/main/java/com/intuit/karate/demo/controller/EncodingController.java @@ -23,10 +23,11 @@ */ package com.intuit.karate.demo.controller; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.HttpServletRequest; + /** * * @author pthomas3 @@ -35,14 +36,28 @@ @RequestMapping("/encoding") public class EncodingController { - @RequestMapping("/{token:.+}") - public String echoPath(@PathVariable String token) { - return token; + @RequestMapping("/**") + public String echoPath(HttpServletRequest httpServletRequest) { + // running from spring boot + tomcat + if (httpServletRequest.getPathInfo() == null) { + return httpServletRequest.getServletPath().replace("/encoding/", ""); + + // running from mock spring mvc + } else { + return httpServletRequest.getPathInfo().replace("/encoding/", ""); + } } - @RequestMapping("/index.php?/api/v2/{token:.+}") - public String echoPathWithQuestion(@PathVariable String token) { - return token; + @RequestMapping("/index.php?/api/v2/**") + public String echoPathWithQuestion(HttpServletRequest httpServletRequest) { + // running from spring boot + tomcat + if (httpServletRequest.getPathInfo() == null) { + return httpServletRequest.getServletPath().replace("/encoding/index.php?/api/v2/", ""); + + // running from mock spring mvc + } else { + return httpServletRequest.getPathInfo().replace("/encoding/index.php?/api/v2/", ""); + } } } diff --git a/karate-demo/src/test/java/demo/DemoTestParallel.java b/karate-demo/src/test/java/demo/DemoTestParallel.java index 8b79706cb..ceda5c8c4 100644 --- a/karate-demo/src/test/java/demo/DemoTestParallel.java +++ b/karate-demo/src/test/java/demo/DemoTestParallel.java @@ -37,7 +37,7 @@ public void testParallel() { public static void generateReport(String karateOutputPath) { Collection jsonFiles = FileUtils.listFiles(new File(karateOutputPath), new String[] {"json"}, true); - List jsonPaths = new ArrayList(jsonFiles.size()); + List jsonPaths = new ArrayList<>(jsonFiles.size()); jsonFiles.forEach(file -> jsonPaths.add(file.getAbsolutePath())); Configuration config = new Configuration(new File("target"), "demo"); ReportBuilder reportBuilder = new ReportBuilder(jsonPaths, config); diff --git a/karate-demo/src/test/java/demo/encoding/encoding.feature b/karate-demo/src/test/java/demo/encoding/encoding.feature index 61478c735..f6c05b21b 100644 --- a/karate-demo/src/test/java/demo/encoding/encoding.feature +++ b/karate-demo/src/test/java/demo/encoding/encoding.feature @@ -21,10 +21,31 @@ Scenario: question mark in the url Then status 200 And match response == 'hello' +Scenario: append trailing / to url + Given url demoBaseUrl + And path 'encoding', 'hello', '' + When method get + Then status 200 + And match response == 'hello/' + +Scenario: path escapes special characters + Given url demoBaseUrl + And path 'encoding', '"<>#{}|\^[]`' + When method get + Then status 200 + And match response == '"<>#{}|\^[]`' + +Scenario: leading / in path is not required + Given url demoBaseUrl + And path 'encoding', 'hello' + When method get + Then status 200 + And match response == 'hello' + Scenario: manually decode before passing to karate - * def encoded = 'encoding%2Ffoo%2Bbar' + * def encoded = 'foo%2Bbar' * def decoded = java.net.URLDecoder.decode(encoded, 'UTF-8') - Given url demoBaseUrl + Given url demoBaseUrl + '/encoding' And path decoded When method get Then status 200 diff --git a/karate-demo/src/test/java/demo/error/no-url.feature b/karate-demo/src/test/java/demo/error/no-url.feature index 408586e65..e787adaec 100644 --- a/karate-demo/src/test/java/demo/error/no-url.feature +++ b/karate-demo/src/test/java/demo/error/no-url.feature @@ -1,11 +1,11 @@ -Feature: No URLfound proper error response +Feature: No URL found proper error response Background: * url demoBaseUrl * configure lowerCaseResponseHeaders = true Scenario: Invalid URL response - Given path '/hello' + Given path 'hello' When method get Then status 404 And match header content-type contains 'application/json' diff --git a/karate-mock-servlet/src/main/java/com/intuit/karate/mock/servlet/MockHttpClient.java b/karate-mock-servlet/src/main/java/com/intuit/karate/mock/servlet/MockHttpClient.java index c21d4c7b5..cb4655113 100644 --- a/karate-mock-servlet/src/main/java/com/intuit/karate/mock/servlet/MockHttpClient.java +++ b/karate-mock-servlet/src/main/java/com/intuit/karate/mock/servlet/MockHttpClient.java @@ -34,6 +34,14 @@ import com.intuit.karate.http.Response; import io.netty.handler.codec.http.cookie.DefaultCookie; import io.netty.handler.codec.http.cookie.ServerCookieEncoder; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -42,13 +50,6 @@ import java.util.List; import java.util.Map; import java.util.function.Function; -import javax.servlet.Servlet; -import javax.servlet.ServletContext; -import javax.servlet.http.Cookie; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; /** * @@ -90,7 +91,10 @@ public Response invoke(HttpRequest hr) { } catch (Exception e) { throw new RuntimeException(e); } - MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.request(request.getMethod(), uri); + MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.request(request.getMethod(), uri) + // Spring is decoding this using ISO 8859-1 instead of UTF-8, so here we explicitly set path info from + // the URI which decoded it using UTF-8. This prevents Spring from having to decode it itself. + .pathInfo(uri.getPath()); if (request.getHeaders() != null) { request.getHeaders().forEach((k, vals) -> builder.header(k, vals.toArray())); request.getCookies().forEach(c -> { @@ -133,7 +137,7 @@ public Response invoke(HttpRequest hr) { } headers = toHeaders(res.getHeaderNames(), name -> res.getHeaders(name)); javax.servlet.http.Cookie[] cookies = res.getCookies(); - List cookieValues = new ArrayList(cookies.length); + List cookieValues = new ArrayList<>(cookies.length); for (javax.servlet.http.Cookie c : cookies) { DefaultCookie dc = new DefaultCookie(c.getName(), c.getValue()); dc.setDomain(c.getDomain()); @@ -152,7 +156,7 @@ public Response invoke(HttpRequest hr) { } private static Collection toCollection(Enumeration values) { - List list = new ArrayList(); + List list = new ArrayList<>(); while (values.hasMoreElements()) { list.add(values.nextElement()); } @@ -160,10 +164,10 @@ private static Collection toCollection(Enumeration values) { } private static Map> toHeaders(Collection names, Function> valuesFn) { - Map> map = new LinkedHashMap(names.size()); + Map> map = new LinkedHashMap<>(names.size()); for (String name : names) { Collection values = valuesFn.apply(name); - List list = new ArrayList(values.size()); + List list = new ArrayList<>(values.size()); for (String value : values) { list.add(value); }