From 39b6817ae9257c514a73e10704d759dc3cd95cb6 Mon Sep 17 00:00:00 2001 From: Igor Polevoy <500941+ipolevoy@users.noreply.github.com> Date: Tue, 13 Dec 2022 19:20:20 -0600 Subject: [PATCH] #1258 - Add convenience methods to controller specs to work with JSON --- .../javalite/activeweb/RequestBuilder.java | 15 +++++ .../javalite/activeweb/RequestSpecHelper.java | 23 ++++++- .../java/app/controllers/JSONController.java | 21 ++++++ .../app/controllers/JSONControllerSpec.java | 64 +++++++++++++++++++ .../org/javalite/activeweb/HttpSupport.java | 42 +++++++++++- .../main/java/org/javalite/json/JSONList.java | 13 ++++ .../main/java/org/javalite/json/JSONMap.java | 10 ++- 7 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 activeweb-testing/src/test/java/app/controllers/JSONController.java create mode 100644 activeweb-testing/src/test/java/app/controllers/JSONControllerSpec.java diff --git a/activeweb-testing/src/main/java/org/javalite/activeweb/RequestBuilder.java b/activeweb-testing/src/main/java/org/javalite/activeweb/RequestBuilder.java index ec1212870..fa71ea05e 100644 --- a/activeweb-testing/src/main/java/org/javalite/activeweb/RequestBuilder.java +++ b/activeweb-testing/src/main/java/org/javalite/activeweb/RequestBuilder.java @@ -270,6 +270,21 @@ public RequestBuilder content(String content) { return content(content.getBytes()); } + + /** + * Convenience method. Expects the content to be some JSON document (object or array). + * This method does NOT check the validity of the document. It is a responsibility of the controller. + *
+ * This method will automatically set the header "Content-Type" to be "application/json", and that is the convenience. + * + * @param content JSON document is expected + * @return self. + */ + public RequestBuilder json(String content) { + contentType("application/json"); + return content(content.getBytes()); + } + /** * Use to post content to a tested controller. * diff --git a/activeweb-testing/src/main/java/org/javalite/activeweb/RequestSpecHelper.java b/activeweb-testing/src/main/java/org/javalite/activeweb/RequestSpecHelper.java index c9a319799..6b88a6dd7 100644 --- a/activeweb-testing/src/main/java/org/javalite/activeweb/RequestSpecHelper.java +++ b/activeweb-testing/src/main/java/org/javalite/activeweb/RequestSpecHelper.java @@ -17,6 +17,8 @@ import org.javalite.common.Convert; +import org.javalite.json.JSONList; +import org.javalite.json.JSONMap; import org.javalite.test.jspec.TestException; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -27,8 +29,6 @@ import java.util.*; -import static org.javalite.common.Util.blank; - /** * This class is not used directly in applications. * @@ -80,6 +80,25 @@ protected String responseContent(){ } } + + /** + * Provides content generated by controller in the {@link JSONMap} format, after controller execution - if views were integrated. + * + * @return content generated by controller/view already converted to JSONMap object. + */ + protected JSONMap responseJSONMap(){ + return new JSONMap(responseContent()); + } + + /** + * Provides content generated by controller in the {@link JSONList} format, after controller execution - if views were integrated. + * + * @return content generated by controller/view already converted to JSONList object. + */ + protected JSONList responseJSONList(){ + return new JSONList(responseContent()); + } + /** * Provides content generated by controller as bytes. * diff --git a/activeweb-testing/src/test/java/app/controllers/JSONController.java b/activeweb-testing/src/test/java/app/controllers/JSONController.java new file mode 100644 index 000000000..f087b8297 --- /dev/null +++ b/activeweb-testing/src/test/java/app/controllers/JSONController.java @@ -0,0 +1,21 @@ +package app.controllers; + +import org.javalite.activeweb.AppController; +import org.javalite.activeweb.annotations.POST; + +import org.javalite.json.JSONMap; + +public class JSONController extends AppController { + + @POST + public void index1(){ + JSONMap request = getRequestJSONMap(); + request.put("last_name", "Doe"); + respondJSON(request); + } + + @POST + public void index2(){ + respondJSON(getRequestJSONList()); + } +} diff --git a/activeweb-testing/src/test/java/app/controllers/JSONControllerSpec.java b/activeweb-testing/src/test/java/app/controllers/JSONControllerSpec.java new file mode 100644 index 000000000..2251e7fe6 --- /dev/null +++ b/activeweb-testing/src/test/java/app/controllers/JSONControllerSpec.java @@ -0,0 +1,64 @@ +package app.controllers; + +import org.javalite.activeweb.ControllerSpec; +import org.javalite.json.JSONList; +import org.javalite.json.JSONMap; +import org.junit.Test; + +public class JSONControllerSpec extends ControllerSpec { + + @Test + public void shouldPostJSONMap(){ + + String doc = """ + { + "first_name" : "John" + } + """; + request().json(doc).post("index1"); + + JSONMap response = responseJSONMap(); + + the(response).shouldContain("first_name"); + the(response).shouldContain("last_name"); + + the(response.get("first_name")).shouldContain("John"); + the(response.get("last_name")).shouldContain("Doe"); + + the(contentType()).shouldBeEqual("application/json"); + } + + @Test + public void shouldPostJSONList(){ + + String doc = """ + [ + "John" , "Doe" + ] + """; + request().json(doc).post("index2"); + + JSONList response = responseJSONList(); + + the(response).shouldContain("John"); + the(response).shouldContain("Doe"); + the(contentType()).shouldBeEqual("application/json"); + } + + @Test + public void shouldPostJSONString(){ + + String doc = """ + [ + "John" , "Doe" + ] + """; + request().json(doc).post("index2"); + + JSONList response = responseJSONList(); + + the(response).shouldContain("John"); + the(response).shouldContain("Doe"); + the(contentType()).shouldBeEqual("application/json"); + } +} diff --git a/activeweb/src/main/java/org/javalite/activeweb/HttpSupport.java b/activeweb/src/main/java/org/javalite/activeweb/HttpSupport.java index b61b69f3e..27d9fef12 100644 --- a/activeweb/src/main/java/org/javalite/activeweb/HttpSupport.java +++ b/activeweb/src/main/java/org/javalite/activeweb/HttpSupport.java @@ -22,6 +22,8 @@ import org.javalite.common.Convert; import org.javalite.json.JSONHelper; import org.javalite.common.Util; +import org.javalite.json.JSONList; +import org.javalite.json.JSONMap; import org.jsoup.Jsoup; import org.jsoup.safety.Safelist; import org.slf4j.Logger; @@ -491,8 +493,7 @@ protected HttpBuilder redirect(Class controllerClas } /** - * This method will send the text to a client verbatim. It will not use any layouts. Use it to build app.services - * and to support AJAX. + * This method will send the text to a client verbatim. It will not use any layouts. Use it to support AJAX or API functions. * * @param text text of response. * @return {@link HttpSupport.HttpBuilder}, to accept additional information. @@ -502,10 +503,28 @@ protected HttpBuilder respond(String text){ text = "null"; } DirectResponse resp = new DirectResponse(text); + String contentType = RequestContext.getHttpRequest().getHeader("Content-Type"); + //TODO: is this some spaghetti code? Seems we have Content-type in RequestContext as well as in ControllerResponse? + if(contentType != null){ + resp.setContentType(contentType); + } + RequestContext.setControllerResponse(resp); return new HttpBuilder(resp); } + /** + * Serializes the argument as a JSON string and sets the "Content-Type" header to "application/json". + * See {@link HttpSupport#respond(String)}. + * + * @param object object to serialize to JSON + * @return self + */ + protected HttpBuilder respondJSON(Object object){ + header("Content-Type", "application/json"); + return respond(JSONHelper.toJsonString(object)); + } + /** * Convenience method for downloading files. This method will force the browser to find a handler(external program) * for this file (content type) and will provide a name of file to the browser. This method sets an HTTP header @@ -1340,6 +1359,25 @@ protected InputStream getRequestStream() { } } + + /** + * Reads entire request data and converts it to {@link JSONMap}. + * + * @return data sent by client as {@link JSONMap}. + */ + protected JSONMap getRequestJSONMap() { + return new JSONMap(getRequestString()); + } + + /** + * Reads entire request data and converts it to {@link JSONList}. + * + * @return data sent by client as {@link JSONList}. + */ + protected JSONList getRequestJSONList() { + return new JSONList(getRequestString()); + } + /** * Reads entire request data as String. Do not use for large data sets to avoid * memory issues, instead use {@link #getRequestInputStream()}. diff --git a/javalite-common/src/main/java/org/javalite/json/JSONList.java b/javalite-common/src/main/java/org/javalite/json/JSONList.java index 8881d94e8..388d00152 100644 --- a/javalite-common/src/main/java/org/javalite/json/JSONList.java +++ b/javalite-common/src/main/java/org/javalite/json/JSONList.java @@ -37,9 +37,15 @@ * @see JSONList#getMap(int) */ public class JSONList extends ArrayList { + + public JSONList(List jsonList){ super(jsonList); } + + /** + * @param jsonList JSON array as string. + */ public JSONList(String jsonList){ super(JSONHelper.toList(jsonList)); } @@ -145,4 +151,11 @@ public String toString() { public String toJSON(boolean pretty){ return JSONHelper.toJsonString(this, pretty); } + + /** + * @return a JSON representation of this object + */ + public String toJSON(){ + return JSONHelper.toJsonString(this, false); + } } diff --git a/javalite-common/src/main/java/org/javalite/json/JSONMap.java b/javalite-common/src/main/java/org/javalite/json/JSONMap.java index c0d07b56e..2c53309b1 100644 --- a/javalite-common/src/main/java/org/javalite/json/JSONMap.java +++ b/javalite-common/src/main/java/org/javalite/json/JSONMap.java @@ -39,6 +39,15 @@ public JSONMap(String ... keysAndValues){ super(map(keysAndValues)); } + /** + * Converts the String into a JSONMap. + * + * @param jsonString JSON Object document as string. + */ + public JSONMap(String jsonString){ + super(JSONHelper.toJSONMap(jsonString)); + } + public JSONMap(){} /** @@ -285,7 +294,6 @@ public String toJSON(boolean pretty){ } /** - * * @return a JSON representation of this object */ public String toJSON(){