Skip to content

Commit

Permalink
Introduce path encoding (fixes karatelabs#1561)
Browse files Browse the repository at this point in the history
HttpRequestBuilder.java
- replaced armeria QueryParamsBuilder with apache http `URIBuilder`.
- minor generics cleanup
- `URIBuilder` now handles path segment encoding and query parameters.
- we now check if any paths are prefixed with `/` and issue a warning log, that this is probably a mistake

encoding.feature
- added more demo/test cases for path encoding

HttpUtils.java
- minor generics cleanup
- final is redundant on static methods

karate-demo/pom.xml
- scope = import only works in the dependencyManagement section, this fixes the maven warning.

Request.java
- minor generics cleanup
- use computeIfAbsent()

HttpClientFactory.java
- public static final are redundant on interface fields
- also use method reference

KarateMockHandlerTest.java
KarateHttpMockHandlerTest.java
- removed all `/` path prefixes

README.md
- updated with details on how to correctly use path
- changes any erroneous examples of path usage
  • Loading branch information
aaronjwhiteside committed Apr 20, 2021
1 parent ff44e06 commit 8b50dc4
Show file tree
Hide file tree
Showing 8 changed files with 65 additions and 82 deletions.
12 changes: 9 additions & 3 deletions README.md
Expand Up @@ -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`.

Expand Down Expand Up @@ -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'
Expand Down
Expand Up @@ -93,7 +93,6 @@ public class HttpRequestBuilder implements ProxyObject {
private String method;
private List<String> paths;
private Map<String, List<String>> params;
private String fragment;
private Map<String, List<String>> headers;
private MultiPartBuilder multiPart;
private Object body;
Expand Down Expand Up @@ -241,11 +240,6 @@ public HttpRequestBuilder method(String method) {
return this;
}

public HttpRequestBuilder fragment(String fragment) {
this.fragment = fragment;
return this;
}

public HttpRequestBuilder paths(String... paths) {
for (String path : paths) {
path(path);
Expand All @@ -264,36 +258,26 @@ public HttpRequestBuilder path(String path) {
return this;
}

private List<String> backwardsCompatiblePaths() {
if (paths == null) {
return Collections.emptyList();
}

List<String> result = new ArrayList<>(paths.size());
for (int i = 0; i < paths.size(); i++) {
String path = paths.get(i);
if (i == 0 && path.startsWith("/")) {
path = path.substring(1);
logger.warn("the first path segment starts with a '/', this will be stripped off for now, but in the future this may be escaped and cause your request to fail.");
}
result.add(path);
}
return result;
}

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)));
}
// merge paths from the base url with additional paths supplied to this builder
List<String> merged = Stream.of(builder.getPathSegments(), backwardsCompatiblePaths())
.flatMap(List::stream)
.collect(Collectors.toList());
return builder.setPathSegments(merged)
.setFragment(fragment)
.build();
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<String> merged = Stream.of(builder.getPathSegments(), paths)
.flatMap(List::stream)
.collect(Collectors.toList());
builder.setPathSegments(merged);
}
return builder.build();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
Expand Down
Expand Up @@ -68,7 +68,7 @@ void testSimpleGet() {
startMockServer();
run(
urlStep(),
"path '/hello'",
"path 'hello'",
"method get"
);
matchVar("response", "hello world");
Expand All @@ -82,7 +82,7 @@ void testThatCookieIsPartOfRequest() {
startMockServer();
run(
urlStep(),
"path '/hello'",
"path 'hello'",
"cookie foo = 'bar'",
"method get"
);
Expand All @@ -97,7 +97,7 @@ void testSameSiteSecureCookieRequest() {
startMockServer();
run(
urlStep(),
"path '/hello'",
"path 'hello'",
"cookie foo = { value: 'bar', samesite: 'Strict', secure: true }",
"method get"
);
Expand All @@ -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'] }");
Expand All @@ -126,7 +126,7 @@ void testThatExoticContentTypeIsPreserved() {
startMockServer();
run(
urlStep(),
"path '/hello'",
"path 'hello'",
"header Content-Type = 'application/xxx.pingixxxxxx.checkUsernamePassword+json'",
"method post"
);
Expand All @@ -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"
);
Expand All @@ -159,7 +159,7 @@ void testKarateRemove() {
startMockServer();
run(
urlStep(),
"path '/hello', '1'",
"path 'hello', '1'",
"method get"
);
matchVarContains("response", "{ '2': 'bar' }");
Expand All @@ -173,7 +173,7 @@ void testTransferEncoding() {
startMockServer();
run(
urlStep(),
"path '/hello'",
"path 'hello'",
"header Transfer-Encoding = 'chunked'",
"request { foo: 'bar' }",
"method post"
Expand All @@ -189,7 +189,7 @@ void testMalformedMockResponse() {
startMockServer();
run(
urlStep(),
"path '/hello'",
"path 'hello'",
"method get",
"match response == '{ \"id\" \"123\" }'",
"match responseType == 'string'"
Expand All @@ -210,7 +210,7 @@ void testRedirectAfterPostWithCookie() {
startMockServer();
run(
urlStep(),
"path '/first'",
"path 'first'",
"form fields { username: 'blah', password: 'blah' }",
"method post"
);
Expand Down

0 comments on commit 8b50dc4

Please sign in to comment.