Skip to content

Commit

Permalink
[rewrite] #1281 mock servlet now handles multi-part
Browse files Browse the repository at this point in the history
  • Loading branch information
ptrthomas committed Nov 14, 2020
1 parent 5ad9783 commit 3066f22
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 64 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# <img src="karate-core/src/main/resources/res/karate-logo.svg" height="60" width="60"/> Karate
## Test Automation Made `Simple.`
[![Maven Central](https://img.shields.io/maven-central/v/com.intuit.karate/karate-core.svg)](https://mvnrepository.com/artifact/com.intuit.karate/karate-core) [ ![build](https://github.com/intuit/karate/workflows/maven-build/badge.svg)](https://github.com/intuit/karate/actions) [![GitHub release](https://img.shields.io/github/release/intuit/karate.svg)](https://github.com/intuit/karate/releases) [![Support Slack](https://img.shields.io/badge/support-slack-red.svg)](https://github.com/intuit/karate/wiki/Support) [![Twitter Follow](https://img.shields.io/twitter/follow/KarateDSL.svg?style=social&label=Follow)](https://twitter.com/KarateDSL)
[![Maven Central](https://img.shields.io/maven-central/v/com.intuit.karate/karate-core.svg)](https://search.maven.org/artifact/com.intuit.karate/karate-core) [ ![build](https://github.com/intuit/karate/workflows/maven-build/badge.svg)](https://github.com/intuit/karate/actions?query=workflow%3Amaven-build) [![GitHub release](https://img.shields.io/github/release/intuit/karate.svg)](https://github.com/intuit/karate/releases) [![Support Slack](https://img.shields.io/badge/support-slack-red.svg)](https://github.com/intuit/karate/wiki/Support) [![Twitter Follow](https://img.shields.io/twitter/follow/KarateDSL.svg?style=social&label=Follow)](https://twitter.com/KarateDSL)

Karate is the only open-source tool to combine API test-automation, [mocks](karate-netty), [performance-testing](karate-gatling) and even [UI automation](karate-core) into a **single**, *unified* framework. The BDD syntax popularized by Cucumber is language-neutral, and easy for even non-programmers. Powerful JSON & XML assertions are built-in, and you can run tests in parallel for speed.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public class MockHandler implements ServerHandler {

private static final String REQUEST_BYTES = "requestBytes";
private static final String REQUEST_PARAMS = "requestParams";
private static final String REQUEST_FILES = "requestFiles";
private static final String REQUEST_PARTS = "requestParts";

private static final String RESPONSE_DELAY = "responseDelay";

Expand Down Expand Up @@ -134,9 +134,9 @@ public Response handle(Request req) {
engine.setVariable(ScenarioEngine.REQUEST, req.getBodyConverted());
engine.setVariable(REQUEST_PARAMS, req.getParams());
engine.setVariable(REQUEST_BYTES, req.getBody());
Map<String, List<Map<String, Object>>> files = req.getMultiPartFiles();
if (files != null) {
engine.setHiddenVariable(REQUEST_FILES, files); // TODO add to docs
Map<String, List<Map<String, Object>>> parts = req.getMultiParts();
if (parts != null) {
engine.setHiddenVariable(REQUEST_PARTS, parts); // TODO add to docs
}
for (FeatureSection fs : feature.getSections()) {
if (fs.isOutline()) {
Expand Down
85 changes: 48 additions & 37 deletions karate-core/src/main/java/com/intuit/karate/server/Request.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,14 @@ public class Request implements ProxyObject {
private static final Set<String> KEY_SET = new HashSet(Arrays.asList(KEYS));
private static final JsArray KEY_ARRAY = new JsArray(KEYS);

private String urlAndPath;
private String urlBase;
private String path;
private String method;
private Map<String, List<String>> params;
private Map<String, List<String>> headers;
private byte[] body;
private Map<String, List<Map<String, Object>>> multiPartFiles;
private Map<String, List<Map<String, Object>>> multiParts;
private ResourceType resourceType;
private String resourcePath;
private String pathParam;
Expand All @@ -118,8 +119,12 @@ public boolean isAjax() {
return getHeader(HttpConstants.HDR_HX_REQUEST) != null;
}

public Map<String, List<Map<String, Object>>> getMultiPartFiles() {
return multiPartFiles;
public boolean isMultiPart() {
return multiParts != null;
}

public Map<String, List<Map<String, Object>>> getMultiParts() {
return multiParts;
}

public List<String> getHeaderValues(String name) {
Expand All @@ -140,21 +145,26 @@ public String getContentType() {
}

public String getParam(String name) {
if (params == null) {
return null;
}
List<String> values = params.get(name);
List<String> values = getParamValues(name);
if (values == null || values.isEmpty()) {
return null;
}
return values.get(0);
}

public List<String> getParamValues(String name) {
if (params == null) {
return null;
}
return params.get(name);
}

public String getPath() {
return path;
}

public void setUrl(String url) {
urlAndPath = url;
StringUtils.Pair pair = NettyUtils.parseUriIntoUrlBaseAndPath(url);
urlBase = pair.left;
QueryStringDecoder qsd = new QueryStringDecoder(pair.right);
Expand All @@ -171,6 +181,10 @@ public void setUrl(String url) {
setParams(queryParams);
}

public String getUrlAndPath() {
return urlAndPath;
}

public String getUrlBase() {
return urlBase;
}
Expand Down Expand Up @@ -279,10 +293,10 @@ public Object getParamAsJsValue(String name) {
}

public Map<String, Object> getMultiPart(String name) {
if (multiPartFiles == null) {
if (multiParts == null) {
return null;
}
List<Map<String, Object>> parts = multiPartFiles.get(name);
List<Map<String, Object>> parts = multiParts.get(name);
if (parts == null || parts.isEmpty()) {
return null;
}
Expand All @@ -304,6 +318,7 @@ public void processBody() {
boolean multipart;
if (contentType.startsWith("multipart")) {
multipart = true;
multiParts = new HashMap();
} else if (contentType.contains("form-urlencoded")) {
multipart = false;
} else {
Expand All @@ -317,39 +332,35 @@ public void processBody() {
try {
for (InterfaceHttpData part : decoder.getBodyHttpDatas()) {
String name = part.getName();
if (part instanceof FileUpload) {
if (multiPartFiles == null) {
multiPartFiles = new HashMap();
}
List<Map<String, Object>> list = multiPartFiles.get(name);
if (multipart) {
List<Map<String, Object>> list = multiParts.get(name);
if (list == null) {
list = new ArrayList();
multiPartFiles.put(name, list);
multiParts.put(name, list);
}
Map<String, Object> map = new HashMap();
list.add(map);
FileUpload fup = (FileUpload) part;
map.put("name", name);
map.put("filename", fup.getFilename());
Charset charset = fup.getCharset();
if (charset != null) {
map.put("charset", charset.name());
}
String ct = fup.getContentType();
map.put("contentType", ct);
ResourceType rt = ResourceType.fromContentType(ct);
Object value;
if (rt.isBinary()) {
value = fup.get();
} else {
value = charset == null ? fup.getString() : fup.getString(charset);
}
map.put("value", value);
String transferEncoding = fup.getContentTransferEncoding();
if (transferEncoding != null) {
map.put("transferEncoding", transferEncoding);
if (part instanceof FileUpload) {
FileUpload fup = (FileUpload) part;
map.put("name", name);
map.put("filename", fup.getFilename());
Charset charset = fup.getCharset();
if (charset != null) {
map.put("charset", charset.name());
}
String ct = fup.getContentType();
map.put("contentType", ct);
map.put("value", fup.get()); // bytes
String transferEncoding = fup.getContentTransferEncoding();
if (transferEncoding != null) {
map.put("transferEncoding", transferEncoding);
}
} else { // simple multi-part key-value pair
Attribute attribute = (Attribute) part;
map.put("name", name);
map.put("value", attribute.getValue());
}
} else { // url-encoded form-field or simple multi-part value
} else { // url-encoded form-field
Attribute attribute = (Attribute) part;
List<String> list = params.get(name);
if (list == null) {
Expand Down Expand Up @@ -394,7 +405,7 @@ public Object getMember(String key) {
case MULTI_PART:
return (Function<String, Object>) this::getMultiPartAsJsValue;
case MULTI_PARTS:
return JsValue.fromJava(multiPartFiles);
return JsValue.fromJava(multiParts);
case GET:
case POST:
case PUT:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,28 +300,28 @@ void testFormFieldPost() {
void testMultiPartField() {
background().scenario(
"pathMatches('/hello')",
"def response = requestParams");
"def response = requestParts");
run(
URL_STEP,
"multipart field foo = 'bar'",
"path '/hello'",
"method post"
);
matchVar("response", "{ foo: ['bar'] }");
matchVar("response", "{ foo: [{ name: 'foo', value: '#notnull' }] }");
}

@Test
void testMultiPartFile() {
background().scenario(
"pathMatches('/hello')",
"def response = requestFiles");
"def response = requestParts");
run(
URL_STEP,
"multipart file foo = { filename: 'foo.txt', value: 'hello' }",
"path '/hello'",
"method post"
);
matchVar("response", "{ foo: [{ name: 'foo', value: 'hello', contentType: 'text/plain', charset: 'UTF-8', filename: 'foo.txt', transferEncoding: '7bit' }] }");
matchVar("response", "{ foo: [{ name: 'foo', value: '#notnull', contentType: 'text/plain', charset: 'UTF-8', filename: 'foo.txt', transferEncoding: '7bit' }] }");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ void testResponseHeaders() {
void testMultiPart() {
background().scenario(
"pathMatches('/hello')",
"def foo = paramValue('foo')",
"string bar = requestFiles.bar[0].value",
"def foo = requestParts.foo[0].value",
"string bar = requestParts.bar[0].value",
"def response = { foo: '#(foo)', bar: '#(bar)' }"
);
request.path("/hello")
Expand Down
4 changes: 1 addition & 3 deletions karate-demo/src/test/java/demo/upload/upload-image.feature
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@mock-servlet-todo
Feature: file upload end-point

Background:
Expand All @@ -21,7 +20,6 @@ Scenario: upload image - multipart
And match header Content-Disposition contains 'karate-logo.jpg'
And match header Content-Type == 'image/jpg'

@mock-servlet-todo
Scenario: upload image - binary request body
Given path 'files', 'binary'
And param name = 'karate-logo.jpg'
Expand All @@ -37,12 +35,12 @@ Scenario: upload image - binary request body
And match responseBytes == read('karate-logo.jpg')
And match header Content-Disposition contains 'karate-logo.jpg'

@mock-servlet-todo
Scenario: upload stream - content-length should be sent correctly
Given path 'search', 'headers'
And param name = 'karate-logo.jpg'
And request read('karate-logo.jpg')
When method post
Then status 200
And def response = karate.lowerCase(response)
And match response['content-length'][0] == '13575'

Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@mock-servlet-todo
Feature: multipart fields (multiple)

Background:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@mock-servlet-todo
Feature: multipart files (multiple)

Background:
Expand Down
1 change: 0 additions & 1 deletion karate-demo/src/test/java/demo/upload/upload-retry.feature
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@mock-servlet-todo
Feature: file upload retry

Background:
Expand Down
2 changes: 1 addition & 1 deletion karate-demo/src/test/java/demo/upload/upload.feature
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@mock-servlet-todo
Feature: file upload end-point

Background:
Expand Down Expand Up @@ -63,6 +62,7 @@ Scenario: upload with content created dynamically
And match header Content-Disposition contains 'hello.txt'
And match header Content-Type contains 'text/plain'

@mock-servlet-todo
Scenario: upload multipart/mixed
Given path 'files', 'mixed'
And multipart field myJson = { value: { text: 'hello world' } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
import com.intuit.karate.server.HttpConstants;
import com.intuit.karate.server.HttpLogger;
import com.intuit.karate.server.HttpRequest;
import com.intuit.karate.server.Request;
import com.intuit.karate.server.Response;
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.CookieDecoder;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import java.net.URI;
Expand Down Expand Up @@ -82,10 +82,12 @@ public Config getConfig() {
}

@Override
public Response invoke(HttpRequest request) {
public Response invoke(HttpRequest hr) {
Request request = hr.toRequest();
request.processBody();
URI uri;
try {
uri = new URI(request.getUrl());
uri = new URI(request.getUrlAndPath());
} catch (Exception e) {
throw new RuntimeException(e);
}
Expand All @@ -110,17 +112,28 @@ public Response invoke(HttpRequest request) {
}
}
}
if (request.getBody() != null) {
builder.content(request.getBody());
}
builder.content(request.getBody());
MockHttpServletResponse res = new MockHttpServletResponse();
MockHttpServletRequest req = builder.buildRequest(servletContext);
if (request.isMultiPart()) {
request.getMultiParts().forEach((name, v) -> {
for (Map<String, Object> map : v) {
String fileName = (String) map.get("filename");
if (fileName != null) {
req.addPart(new MockPart(map));
} else {
String value = (String) map.get("value");
req.addParameter(name, value);
}
}
});
}
Map<String, List<String>> headers = toHeaders(toCollection(req.getHeaderNames()), name -> toCollection(req.getHeaders(name)));
request.setHeaders(headers);
httpLogger.logRequest(engine.getConfig(), request);
httpLogger.logRequest(engine.getConfig(), hr);
try {
servlet.service(req, res);
request.setEndTimeMillis(System.currentTimeMillis());
hr.setEndTimeMillis(System.currentTimeMillis());
} catch (Exception e) {
throw new RuntimeException(e);
}
Expand All @@ -140,7 +153,7 @@ public Response invoke(HttpRequest request) {
headers.put(HttpConstants.HDR_SET_COOKIE, cookieValues);
}
Response response = new Response(res.getStatus(), headers, res.getContentAsByteArray());
httpLogger.logResponse(getConfig(), request, response);
httpLogger.logResponse(getConfig(), hr, response);
return response;
}

Expand Down
Loading

0 comments on commit 3066f22

Please sign in to comment.