package data; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonPrimitive; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.junit.Test; import static org.junit.Assert.*; import org.junit.Before; import utils.TwitterDateAdapter; /** * Tests whether the ValidatingJsonDeserializer class properly rejects * incomplete or malformed Tweet or User objects. * * @author Peter Wu */ public class ValidatingJsonDeserializerTest { private final static String exampleTweet = "{" + " \"retweeted\": false," + " \"in_reply_to_screen_name\": null," + " \"possibly_sensitive\": false," + " \"truncated\": false," + " \"lang\": \"en\"," + " \"in_reply_to_status_id_str\": null," + " \"id\": 461441338630619136," + " \"in_reply_to_user_id_str\": null," + " \"in_reply_to_status_id\": null," + " \"created_at\": \"Wed Apr 30 09:46:02 +0000 2014\"," + " \"favorite_count\": 0," + " \"place\": null," + " \"coordinates\": null," + " \"metadata\": {" + " \"result_type\": \"recent\"," + " \"iso_language_code\": \"en\"" + " }," + " \"text\": \"RT @verge: Samsung's Galaxy K zoom is a high-end smartphone and an incredible selfie machine http://t.co/I5WmnHV9HM http://t.co/V8Pm2bxzcH\"," + " \"contributors\": null," + " \"retweeted_status\": {" + " \"contributors\": null," + " \"text\": \"Samsung's Galaxy K zoom is a high-end smartphone and an incredible selfie machine http://t.co/I5WmnHV9HM http://t.co/V8Pm2bxzcH\"," + " \"geo\": null," + " \"retweeted\": false," + " \"in_reply_to_screen_name\": null," + " \"possibly_sensitive\": false," + " \"truncated\": false," + " \"lang\": \"en\"," + " \"entities\": {" + " \"symbols\": []," + " \"urls\": [{" + " \"expanded_url\": \"http://vrge.co/1ivZTla\"," + " \"indices\": [82, 104]," + " \"display_url\": \"vrge.co/1ivZTla\"," + " \"url\": \"http://t.co/I5WmnHV9HM\"" + " }]," + " \"hashtags\": []," + " \"media\": [{" + " \"sizes\": {" + " \"small\": {" + " \"w\": 340," + " \"resize\": \"fit\"," + " \"h\": 225" + " }," + " \"thumb\": {" + " \"w\": 150," + " \"resize\": \"crop\"," + " \"h\": 150" + " }," + " \"large\": {" + " \"w\": 1020," + " \"resize\": \"fit\"," + " \"h\": 675" + " }," + " \"medium\": {" + " \"w\": 599," + " \"resize\": \"fit\"," + " \"h\": 397" + " }" + " }," + " \"id\": 461125128559263744," + " \"media_url_https\": \"https://pbs.twimg.com/media/BmY-3HzIAAAR1d6.jpg\"," + " \"media_url\": \"http://pbs.twimg.com/media/BmY-3HzIAAAR1d6.jpg\"," + " \"expanded_url\": \"http://twitter.com/verge/status/461125128710262784/photo/1\"," + " \"indices\": [105, 127]," + " \"id_str\": \"461125128559263744\"," + " \"type\": \"photo\"," + " \"display_url\": \"pic.twitter.com/V8Pm2bxzcH\"," + " \"url\": \"http://t.co/V8Pm2bxzcH\"" + " }]," + " \"user_mentions\": []" + " }," + " \"in_reply_to_status_id_str\": null," + " \"id\": 461125128710262784," + " \"source\": \"Twitter for Mac<\\/a>\"," + " \"in_reply_to_user_id_str\": null," + " \"favorited\": false," + " \"in_reply_to_status_id\": null," + " \"retweet_count\": 113," + " \"created_at\": \"Tue Apr 29 12:49:32 +0000 2014\"," + " \"in_reply_to_user_id\": null," + " \"favorite_count\": 79," + " \"id_str\": \"461125128710262784\"," + " \"place\": null," + " \"user\": {" + " \"location\": \"The Universe\"," + " \"default_profile\": false," + " \"profile_background_tile\": false," + " \"statuses_count\": 45365," + " \"lang\": \"en\"," + " \"profile_link_color\": \"FA4D2A\"," + " \"profile_banner_url\": \"https://pbs.twimg.com/profile_banners/275686563/1348153919\"," + " \"id\": 275686563," + " \"following\": null," + " \"protected\": false," + " \"favourites_count\": 679," + " \"profile_text_color\": \"333333\"," + " \"description\": \"The culture of now. We tell stories at the intersection of science, technology, art, and the humanities.\"," + " \"verified\": true," + " \"contributors_enabled\": false," + " \"profile_sidebar_border_color\": \"FFFFFF\"," + " \"name\": \"The Verge\"," + " \"profile_background_color\": \"FFFFFF\"," + " \"created_at\": \"Fri Apr 01 19:54:22 +0000 2011\"," + " \"is_translation_enabled\": true," + " \"default_profile_image\": false," + " \"followers_count\": 442637," + " \"profile_image_url_https\": \"https://pbs.twimg.com/profile_images/1450127603/TheVerge_Badge_Color1_normal.png\"," + " \"geo_enabled\": false," + " \"profile_background_image_url\": \"http://pbs.twimg.com/profile_background_images/378800000021313532/66906cdb8cdbfe2de5ac7ada880ca029.png\"," + " \"profile_background_image_url_https\": \"https://pbs.twimg.com/profile_background_images/378800000021313532/66906cdb8cdbfe2de5ac7ada880ca029.png\"," + " \"follow_request_sent\": null," + " \"entities\": {" + " \"description\": {" + " \"urls\": []" + " }," + " \"url\": {" + " \"urls\": [{" + " \"expanded_url\": \"http://www.theverge.com\"," + " \"indices\": [0, 22]," + " \"display_url\": \"theverge.com\"," + " \"url\": \"http://t.co/W2SFxIGhA4\"" + " }]" + " }" + " }," + " \"url\": \"http://t.co/W2SFxIGhA4\"," + " \"utc_offset\": -14400," + " \"time_zone\": \"Eastern Time (US & Canada)\"," + " \"notifications\": null," + " \"profile_use_background_image\": true," + " \"friends_count\": 112," + " \"profile_sidebar_fill_color\": \"EFEFEF\"," + " \"screen_name\": \"verge\"," + " \"id_str\": \"275686563\"," + " \"profile_image_url\": \"http://pbs.twimg.com/profile_images/1450127603/TheVerge_Badge_Color1_normal.png\"," + " \"listed_count\": 17450," + " \"is_translator\": false" + " }," + " \"coordinates\": null," + " \"metadata\": {" + " \"result_type\": \"recent\"," + " \"iso_language_code\": \"en\"" + " }" + " }," + " \"geo\": null," + " \"entities\": {" + " \"symbols\": []," + " \"urls\": [{" + " \"expanded_url\": \"http://vrge.co/1ivZTla\"," + " \"indices\": [93, 115]," + " \"display_url\": \"vrge.co/1ivZTla\"," + " \"url\": \"http://t.co/I5WmnHV9HM\"" + " }]," + " \"hashtags\": []," + " \"media\": [{" + " \"sizes\": {" + " \"small\": {" + " \"w\": 340," + " \"resize\": \"fit\"," + " \"h\": 225" + " }," + " \"thumb\": {" + " \"w\": 150," + " \"resize\": \"crop\"," + " \"h\": 150" + " }," + " \"large\": {" + " \"w\": 1020," + " \"resize\": \"fit\"," + " \"h\": 675" + " }," + " \"medium\": {" + " \"w\": 599," + " \"resize\": \"fit\"," + " \"h\": 397" + " }" + " }," + " \"id\": 461125128559263744," + " \"media_url_https\": \"https://pbs.twimg.com/media/BmY-3HzIAAAR1d6.jpg\"," + " \"media_url\": \"http://pbs.twimg.com/media/BmY-3HzIAAAR1d6.jpg\"," + " \"expanded_url\": \"http://twitter.com/verge/status/461125128710262784/photo/1\"," + " \"indices\": [116, 138]," + " \"id_str\": \"461125128559263744\"," + " \"type\": \"photo\"," + " \"display_url\": \"pic.twitter.com/V8Pm2bxzcH\"," + " \"url\": \"http://t.co/V8Pm2bxzcH\"" + " }]," + " \"user_mentions\": [{" + " \"id\": 275686563," + " \"name\": \"The Verge\"," + " \"indices\": [3, 9]," + " \"screen_name\": \"verge\"," + " \"id_str\": \"275686563\"" + " }]" + " }," + " \"source\": \"Twitter for Android Tablets<\\/a>\"," + " \"favorited\": false," + " \"in_reply_to_user_id\": null," + " \"retweet_count\": 113," + " \"id_str\": \"461441338630619136\"," + " \"user\": {" + " \"location\": \"GURGAON\"," + " \"default_profile\": false," + " \"profile_background_tile\": true," + " \"statuses_count\": 2208," + " \"lang\": \"en\"," + " \"profile_link_color\": \"2FC2EF\"," + " \"profile_banner_url\": \"https://pbs.twimg.com/profile_banners/106707648/1398635609\"," + " \"id\": 106707648," + " \"following\": null," + " \"protected\": false," + " \"favourites_count\": 547," + " \"profile_text_color\": \"666666\"," + " \"description\": \"A WANNABE WRITER.....\"," + " \"verified\": false," + " \"contributors_enabled\": false," + " \"profile_sidebar_border_color\": \"FFFFFF\"," + " \"name\": \"MANGO PEOPLE\"," + " \"profile_background_color\": \"1A1B1F\"," + " \"created_at\": \"Wed Jan 20 12:59:53 +0000 2010\"," + " \"is_translation_enabled\": false," + " \"default_profile_image\": false," + " \"followers_count\": 573," + " \"profile_image_url_https\": \"https://pbs.twimg.com/profile_images/461180974823272448/RpSM6o2f_normal.jpeg\"," + " \"geo_enabled\": true," + " \"profile_background_image_url\": \"http://pbs.twimg.com/profile_background_images/378800000157705856/1unyAtzk.jpeg\"," + " \"profile_background_image_url_https\": \"https://pbs.twimg.com/profile_background_images/378800000157705856/1unyAtzk.jpeg\"," + " \"follow_request_sent\": null," + " \"entities\": {" + " \"description\": {" + " \"urls\": []" + " }," + " \"url\": {" + " \"urls\": [{" + " \"expanded_url\": \"http://FACEBOOK.COM/BENXROCKS\"," + " \"indices\": [0, 22]," + " \"display_url\": \"FACEBOOK.COM/BENXROCKS\"," + " \"url\": \"http://t.co/Uwbbg0oN3T\"" + " }]" + " }" + " }," + " \"url\": \"http://t.co/Uwbbg0oN3T\"," + " \"utc_offset\": 19800," + " \"time_zone\": \"New Delhi\"," + " \"notifications\": null," + " \"profile_use_background_image\": true," + " \"friends_count\": 1719," + " \"profile_sidebar_fill_color\": \"252429\"," + " \"screen_name\": \"BH4NUKALRA\"," + " \"id_str\": \"106707648\"," + " \"profile_image_url\": \"http://pbs.twimg.com/profile_images/461180974823272448/RpSM6o2f_normal.jpeg\"," + " \"listed_count\": 2," + " \"is_translator\": false" + " }" + "}"; private Gson gson; @Before public void setUp() { gson = TwitterJsonDeserializer.getGsonBuilder().create(); } private Object deserializeJson(String json, Class cls) { return gson.fromJson(json, cls); } @Test public void testUserEmpty() { try { deserializeJson("{}", User.class); fail("Empty object literal should be rejected."); } catch (JsonParseException ex) { // assume id to be the first field assertEquals("Missing field: id", ex.getMessage()); } } @Test public void testTweetEmpty() { try { deserializeJson("{}", Tweet.class); fail("Empty object literal should be rejected."); } catch (JsonParseException ex) { // assume id to be the first field assertEquals("Missing field: id", ex.getMessage()); } } /** * Test of deserialize method, of class ValidatingJsonDeserializer. */ @Test public void testDeserializeTweet() { Tweet tweet = (Tweet) deserializeJson(exampleTweet, Tweet.class); assertEquals(461441338630619136L, tweet.id); assertNotNull(tweet.retweeted_status); assertEquals("Wed Apr 30 09:46:02 +0000 2014", TwitterDateAdapter.formatDateTime(tweet.created_at)); assertNotNull(tweet.user); assertEquals("GURGAON", tweet.user.location); assertNotNull(tweet.user.entities); assertNotNull(tweet.user.entities.url); assertEquals(1, tweet.user.entities.url.urls.length); assertEquals("http://FACEBOOK.COM/BENXROCKS", tweet.user.entities.url.urls[0].expanded_url); assertEquals("en", tweet.retweeted_status.user.lang); assertEquals(1, tweet.entities.user_mentions.length); assertEquals(275686563, tweet.entities.user_mentions[0].id); assertEquals(0, tweet.entities.hashtags.length); assertEquals(1, tweet.entities.urls.length); assertEquals("http://vrge.co/1ivZTla", tweet.entities.urls[0].expanded_url); } private JsonObject buildMinimalUser() { JsonObject user = new JsonObject(); user.addProperty("id", 4); user.addProperty("name", "X"); user.addProperty("time_zone", "X"); user.addProperty("statuses_count", 4); user.addProperty("followers_count", 4); user.addProperty("friends_count", 4); user.addProperty("location", "X"); user.addProperty("screen_name", "X"); user.addProperty("created_at", "Wed Apr 30 09:46:02 -0300 2014"); JsonObject entities = new JsonObject(); JsonObject urlEntity = new JsonObject(); urlEntity.add("urls", new JsonArray()); entities.add("url", urlEntity); user.add("entities", entities); user.addProperty("lang", "X"); user.addProperty("description", "X"); user.addProperty("verified", false); return user; } private JsonObject buildMinimalTweet(JsonObject user) { JsonObject tweet = new JsonObject(); tweet.addProperty("id", 4); tweet.addProperty("lang", "X"); tweet.addProperty("in_reply_to_status_id", 4); tweet.addProperty("created_at", "Wed Apr 30 09:46:02 +0300 2014"); tweet.addProperty("favorite_count", 4); tweet.add("place", JsonNull.INSTANCE); tweet.add("coordinates", JsonNull.INSTANCE); tweet.addProperty("text", "X"); tweet.add("retweeted_status", JsonNull.INSTANCE); // Tweet object JsonObject entities = new JsonObject(); entities.add("hashtags", new JsonArray()); entities.add("urls", new JsonArray()); entities.add("user_mentions", new JsonArray()); tweet.add("entities", entities); tweet.addProperty("retweet_count", 4); tweet.add("user", user); return tweet; } private void removeProperty(JsonObject obj, String... names) { String prop; for (int i = 0; i < names.length - 1; i++) { prop = names[i]; assert obj.has(prop) : "Missing object property: " + prop; obj = obj.getAsJsonObject(prop); } prop = names[names.length - 1]; assert obj.has(prop) : "Cannot remove missing property: " + prop; obj.remove(prop); } private void addProperty(JsonObject obj, JsonElement val, String... names) { String prop; for (int i = 0; i < names.length - 1; i++) { prop = names[i]; if (!obj.has(prop)) { obj.add(prop, new JsonObject()); } obj = obj.getAsJsonObject(prop); } prop = names[names.length - 1]; obj.add(prop, val); } private void checkImpairedTweet(String... names) { JsonObject tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, names); String prop = StringUtils.join(names, "."); checkTweetFail(tweet, "Missing field: " + prop); } private void checkImpairedUser(String... names) { JsonObject user = buildMinimalUser(); removeProperty(user, names); JsonObject tweet = buildMinimalTweet(user); String prop = StringUtils.join(names, "."); checkTweetFail(tweet, "Missing field: user." + prop); } private void checkTweetFail(JsonObject tweet, String exp) { Gson gson = new Gson(); System.out.println("Checking for failure: " + exp); String json = gson.toJson(tweet); try { deserializeJson(json, Tweet.class); System.out.println("JSON that should have failed: " + json); fail("Expected: " + exp); } catch (JsonParseException ex) { System.out.println("Failure message: " + ex.getMessage()); assertEquals(exp, ex.getMessage()); } } private Tweet checkTweetPass(JsonObject tweet) { Gson gson = new Gson(); String json = gson.toJson(tweet); return (Tweet) deserializeJson(json, Tweet.class); } @Test public void testMinimal() { checkTweetPass(buildMinimalTweet(buildMinimalUser())); checkTweetFail(buildMinimalTweet(null), "Missing field: user"); } @Test public void testTweet() { checkImpairedTweet("id"); checkImpairedTweet("created_at"); checkImpairedTweet("favorite_count"); checkImpairedTweet("text"); checkImpairedTweet("retweet_count"); checkImpairedTweet("user"); } @Test public void testTweetNulls() { // place can be null JsonObject tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, "place"); checkTweetPass(tweet); tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, "in_reply_to_status_id"); checkTweetPass(tweet); tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, "coordinates"); checkTweetPass(tweet); tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, "lang"); checkTweetPass(tweet); } @Test public void testTweetCoordinates() { JsonObject tweet = buildMinimalTweet(buildMinimalUser()); addProperty(tweet, new JsonPrimitive("X"), "coordinates"); checkTweetFail(tweet, "Expected object: coordinates"); JsonObject coords = new JsonObject(); // overwrite coordinates with object addProperty(tweet, coords, "coordinates"); checkTweetFail(tweet, "Missing field: coordinates.coordinates"); // set coordinates.coordinates JsonArray coordsFloat = new JsonArray(); coords.add("coordinates", coordsFloat); checkTweetFail(tweet, "Array smaller than 2: coordinates.coordinates"); coordsFloat.add(new JsonPrimitive(1.0f)); checkTweetFail(tweet, "Array smaller than 2: coordinates.coordinates"); coordsFloat.add(new JsonPrimitive(1.0f)); checkTweetPass(tweet); coordsFloat.add(new JsonPrimitive(1.0f)); checkTweetFail(tweet, "Array larger than 2: coordinates.coordinates"); } @Test public void testTweetEntities() { checkImpairedTweet("entities"); checkImpairedTweet("entities", "hashtags"); checkImpairedTweet("entities", "urls"); } @Test public void testUser() { checkImpairedUser("id"); checkImpairedUser("name"); checkImpairedUser("statuses_count"); checkImpairedUser("followers_count"); checkImpairedUser("friends_count"); checkImpairedUser("screen_name"); checkImpairedUser("created_at"); checkImpairedUser("lang"); checkImpairedUser("verified"); } @Test public void testNullUserFields() { // entities can be null JsonObject tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, "user", "entities"); checkTweetPass(tweet); tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, "user", "time_zone"); checkTweetPass(tweet); tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, "user", "location"); checkTweetPass(tweet); tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, "user", "description"); checkTweetPass(tweet); } @Test public void testUserEntities() { checkImpairedTweet("user", "entities", "url"); checkImpairedTweet("user", "entities", "url", "urls"); } @Test public void testRetweetedStatus() { JsonObject tweet = buildMinimalTweet(buildMinimalUser()); JsonObject rtUser = buildMinimalUser(); String screen_name = "Somebody"; rtUser.addProperty("screen_name", screen_name); JsonObject retweet = buildMinimalTweet(rtUser); tweet.add("retweeted_status", retweet); Tweet parsedTweet = checkTweetPass(tweet); assertNotNull(parsedTweet.retweeted_status); assertEquals(screen_name, parsedTweet.retweeted_status.user.screen_name); } @Test public void testRetweetedMissingText() { JsonObject tweet = buildMinimalTweet(buildMinimalUser()); JsonObject retweet = buildMinimalTweet(buildMinimalUser()); removeProperty(retweet, "text"); tweet.add("retweeted_status", retweet); checkTweetFail(tweet, "Missing field: retweeted_status.text"); } @Test public void testNullTweetFields() { JsonObject tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, "place"); tweet.add("place", null); checkTweetPass(tweet); tweet = buildMinimalTweet(buildMinimalUser()); removeProperty(tweet, "lang"); checkTweetPass(tweet); } @Test public void testWrongType() { JsonObject tweet = buildMinimalTweet(buildMinimalUser()); addProperty(tweet, new JsonPrimitive(1), "text"); checkTweetFail(tweet, "Expected string: text"); } @Test public void testWrongTypeArray() { JsonObject tweet = buildMinimalTweet(buildMinimalUser()); addProperty(tweet, new JsonPrimitive(1), "entities", "urls"); checkTweetFail(tweet, "Expected array: entities.urls"); } @Test public void testWrongTypeObject() { JsonObject tweet = buildMinimalTweet(buildMinimalUser()); addProperty(tweet, new JsonPrimitive(1), "entities"); checkTweetFail(tweet, "Expected object: entities"); } @Test public void testNotATweetObject() { try { DebuggingJsonDeserializer.tryValidate("{}", Tweet.class); fail("DebuggingJsonDeserializer must also check for type."); } catch (JsonParseException ex) { assertEquals("Missing field: id", ex.getMessage()); } } @Test public void testDate() { try { deserializeJson("{\"date\":\"X\"}", DateContainer.class); fail("Invalid date should not pass."); } catch (JsonParseException ex) { assertEquals("Invalid date: X", ex.getMessage()); } } @Test public void testDateValid() { String json = "{\"date\":\"Sat May 10 16:39:34 +1000 2014\"}"; DateContainer d = (DateContainer) deserializeJson(json, DateContainer.class); String serialized = gson.toJson(d, DateContainer.class); assertEquals(json, serialized); } private static class DateContainer { public DateTime date; } }