/* * Copyright (c) 2015-present, Parse, LLC. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.parse; import android.os.Parcel; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import bolts.Capture; import bolts.Task; import bolts.TaskCompletionSource; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseObjectTest { @Rule public ExpectedException thrown = ExpectedException.none(); @Before public void setUp() { ParseFieldOperations.registerDefaultDecoders(); // to test JSON / Parcel decoding } @After public void tearDown() { ParseCorePlugins.getInstance().reset(); } @Test public void testFromJSONPayload() throws JSONException { JSONObject json = new JSONObject( "{" + "\"className\":\"GameScore\"," + "\"createdAt\":\"2015-06-22T21:23:41.733Z\"," + "\"objectId\":\"TT1ZskATqS\"," + "\"updatedAt\":\"2015-06-22T22:06:18.104Z\"," + "\"score\":{" + "\"__op\":\"Increment\"," + "\"amount\":1" + "}," + "\"age\":33" + "}"); ParseObject parseObject = ParseObject.fromJSONPayload(json, ParseDecoder.get()); assertEquals("GameScore", parseObject.getClassName()); assertEquals("TT1ZskATqS", parseObject.getObjectId()); ParseDateFormat format = ParseDateFormat.getInstance(); assertTrue(parseObject.getCreatedAt().equals(format.parse("2015-06-22T21:23:41.733Z"))); assertTrue(parseObject.getUpdatedAt().equals(format.parse("2015-06-22T22:06:18.104Z"))); Set<String> keys = parseObject.getState().keySet(); assertEquals(0, keys.size()); ParseOperationSet currentOperations = parseObject.operationSetQueue.getLast(); assertEquals(2, currentOperations.size()); } @Test public void testFromJSONPayloadWithoutClassname() throws JSONException { JSONObject json = new JSONObject("{\"objectId\":\"TT1ZskATqS\"}"); ParseObject parseObject = ParseObject.fromJSONPayload(json, ParseDecoder.get()); assertNull(parseObject); } //region testRevert @Test public void testRevert() throws ParseException { List<Task<Void>> tasks = new ArrayList<>(); // Mocked to let save work mockCurrentUserController(); // Mocked to simulate in-flight save TaskCompletionSource<ParseObject.State> tcs = mockObjectControllerForSave(); // New clean object ParseObject object = new ParseObject("TestObject"); object.revert("foo"); // Reverts changes on new object object.put("foo", "bar"); object.put("name", "grantland"); object.revert(); assertNull(object.get("foo")); assertNull(object.get("name")); // Object from server ParseObject.State state = mock(ParseObject.State.class); when(state.className()).thenReturn("TestObject"); when(state.objectId()).thenReturn("test_id"); when(state.keySet()).thenReturn(Collections.singleton("foo")); when(state.get("foo")).thenReturn("bar"); object = ParseObject.from(state); object.revert(); assertFalse(object.isDirty()); assertEquals("bar", object.get("foo")); // Reverts changes on existing object object.put("foo", "baz"); object.put("name", "grantland"); object.revert(); assertFalse(object.isDirty()); assertEquals("bar", object.get("foo")); assertFalse(object.isDataAvailable("name")); // Shouldn't revert changes done before last call to `save` object.put("foo", "baz"); object.put("name", "nlutsenko"); tasks.add(object.saveInBackground()); object.revert(); assertFalse(object.isDirty()); assertEquals("baz", object.get("foo")); assertEquals("nlutsenko", object.get("name")); // Should revert changes done after last call to `save` object.put("foo", "qux"); object.put("name", "grantland"); object.revert(); assertFalse(object.isDirty()); assertEquals("baz", object.get("foo")); assertEquals("nlutsenko", object.get("name")); // Allow save to complete tcs.setResult(state); ParseTaskUtils.wait(Task.whenAll(tasks)); } @Test public void testRevertKey() throws ParseException { List<Task<Void>> tasks = new ArrayList<>(); // Mocked to let save work mockCurrentUserController(); // Mocked to simulate in-flight save TaskCompletionSource<ParseObject.State> tcs = mockObjectControllerForSave(); // New clean object ParseObject object = new ParseObject("TestObject"); object.revert("foo"); // Reverts changes on new object object.put("foo", "bar"); object.put("name", "grantland"); object.revert("foo"); assertNull(object.get("foo")); assertEquals("grantland", object.get("name")); // Object from server ParseObject.State state = mock(ParseObject.State.class); when(state.className()).thenReturn("TestObject"); when(state.objectId()).thenReturn("test_id"); when(state.keySet()).thenReturn(Collections.singleton("foo")); when(state.get("foo")).thenReturn("bar"); object = ParseObject.from(state); object.revert("foo"); assertFalse(object.isDirty()); assertEquals("bar", object.get("foo")); // Reverts changes on existing object object.put("foo", "baz"); object.put("name", "grantland"); object.revert("foo"); assertEquals("bar", object.get("foo")); assertEquals("grantland", object.get("name")); // Shouldn't revert changes done before last call to `save` object.put("foo", "baz"); object.put("name", "nlutsenko"); tasks.add(object.saveInBackground()); object.revert("foo"); assertEquals("baz", object.get("foo")); assertEquals("nlutsenko", object.get("name")); // Should revert changes done after last call to `save` object.put("foo", "qux"); object.put("name", "grantland"); object.revert("foo"); assertEquals("baz", object.get("foo")); assertEquals("grantland", object.get("name")); // Allow save to complete tcs.setResult(state); ParseTaskUtils.wait(Task.whenAll(tasks)); } //endregion //region testGetter @Test( expected = IllegalStateException.class ) public void testGetUnavailable() { ParseObject.State state = mock(ParseObject.State.class); when(state.className()).thenReturn("TestObject"); when(state.isComplete()).thenReturn(false); ParseObject object = ParseObject.from(state); object.get("foo"); } @Test public void testGetAvailableIfKeyAvailable() { ParseObject.State state = mock(ParseObject.State.class); when(state.className()).thenReturn("TestObject"); when(state.isComplete()).thenReturn(false); when(state.availableKeys()).thenReturn(new HashSet<>(Arrays.asList("foo"))); ParseObject object = ParseObject.from(state); object.get("foo"); } @Test public void testGetList() throws Exception { ParseObject object = new ParseObject("Test"); JSONArray array = new JSONArray(); array.put("value"); array.put("valueAgain"); object.put("key", array); List list = object.getList("key"); assertEquals(2, list.size()); assertTrue(list.contains("value")); assertTrue(list.contains("valueAgain")); } @Test public void testGetListWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 1); assertNull(object.getList("key")); } @Test public void testGetJSONArray() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", Arrays.asList("value", "valueAgain")); JSONArray array = object.getJSONArray("key"); assertEquals(2, array.length()); assertEquals("value", array.getString(0)); assertEquals("valueAgain", array.getString(1)); } @Test public void testGetJsonArrayWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 1); assertNull(object.getJSONArray("key")); } @Test public void testGetJSONObject() throws Exception { ParseObject object = new ParseObject("Test"); Map<String, String> map = new HashMap<>(); map.put("key", "value"); map.put("keyAgain", "valueAgain"); object.put("key", map); JSONObject json = object.getJSONObject("key"); assertEquals(2, json.length()); assertEquals("value", json.getString("key")); assertEquals("valueAgain", json.getString("keyAgain")); } @Test public void testGetJsonObjectWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 1); assertNull(object.getJSONObject("key")); } @Test public void testGetBoolean() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", true); assertTrue(object.getBoolean("key")); } @Test public void testGetBooleanWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 1); assertFalse(object.getBoolean("key")); } @Test public void testGetDate() throws Exception { ParseObject object = new ParseObject("Test"); Date date = new Date(); object.put("key", date); assertEquals(date, object.getDate("key")); } @Test public void testGetDateWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 1); assertNull(object.getDate("key")); } @Test public void testGetParseGeoPoint() throws Exception { ParseObject object = new ParseObject("Test"); ParseGeoPoint point = new ParseGeoPoint(10, 10); object.put("key", point); assertEquals(point, object.getParseGeoPoint("key")); } @Test public void testGetParseGeoPointWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 1); assertNull(object.getParseGeoPoint("key")); } @Test public void testGetACL() throws Exception { ParseObject object = new ParseObject("Test"); ParseACL acl = new ParseACL(); object.put("ACL", acl); assertEquals(acl, object.getACL()); } @Test public void testGetACLWithSharedACL() throws Exception { ParseObject object = new ParseObject("Test"); ParseACL acl = new ParseACL(); acl.setShared(true); acl.setPublicReadAccess(true); object.put("ACL", acl); ParseACL aclAgain = object.getACL(); assertTrue(aclAgain.getPublicReadAccess()); } @Test public void testGetACLWithNullValue() throws Exception { ParseObject object = new ParseObject("Test"); assertNull(object.getACL()); } @Test public void testGetACLWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("ACL", 1); thrown.expect(RuntimeException.class); thrown.expectMessage("only ACLs can be stored in the ACL key"); object.getACL(); } @Test public void testGetMap() throws Exception { ParseObject object = new ParseObject("Test"); JSONObject json = new JSONObject(); json.put("key", "value"); json.put("keyAgain", "valueAgain"); object.put("key", json); Map map = object.getMap("key"); assertEquals(2, map.size()); assertEquals("value", map.get("key")); assertEquals("valueAgain", map.get("keyAgain")); } @Test public void testGetMapWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 1); assertNull(object.getMap("key")); } @Test public void testGetParseUser() throws Exception { ParseObject object = new ParseObject("Test"); ParseUser user = mock(ParseUser.class); object.put("key", user); assertEquals(user, object.getParseUser("key")); } @Test public void testGetParseUserWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 1); assertNull(object.getParseUser("key")); } @Test public void testGetParseFile() throws Exception { ParseObject object = new ParseObject("Test"); ParseFile file = mock(ParseFile.class); object.put("key", file); assertEquals(file, object.getParseFile("key")); } @Test public void testGetParseFileWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 1); assertNull(object.getParseFile("key")); } @Test public void testGetDouble() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 1.1); assertEquals(1.1, object.getDouble("key"), 0.00001); } @Test public void testGetDoubleWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", "str"); assertEquals(0.0, object.getDouble("key"), 0.00001); } @Test public void testGetLong() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", 10L); assertEquals(10L, object.getLong("key")); } @Test public void testGetLongWithWrongValue() throws Exception { ParseObject object = new ParseObject("Test"); object.put("key", "str"); assertEquals(0, object.getLong("key")); } //endregion //region testParcelable @Test public void testParcelable() throws Exception { ParseObject object = ParseObject.createWithoutData("Test", "objectId"); object.isDeleted = true; object.put("long", 200L); object.put("double", 30D); object.put("int", 50); object.put("string", "test"); object.put("date", new Date(200)); object.put("null", JSONObject.NULL); // Collection object.put("collection", Arrays.asList("test1", "test2")); // Pointer ParseObject other = ParseObject.createWithoutData("Test", "otherId"); object.put("pointer", other); // Map Map<String, Object> map = new HashMap<>(); map.put("key1", "value"); map.put("key2", 50); object.put("map", map); // Bytes byte[] bytes = new byte[2]; object.put("bytes", bytes); // ACL ParseACL acl = new ParseACL(); acl.setReadAccess("reader", true); object.setACL(acl); // Relation ParseObject related = ParseObject.createWithoutData("RelatedClass", "relatedId"); ParseRelation<ParseObject> rel = new ParseRelation<>(object, "relation"); rel.add(related); object.put("relation", rel); // File ParseFile file = new ParseFile(new ParseFile.State.Builder().url("fileUrl").build()); object.put("file", file); // GeoPoint ParseGeoPoint point = new ParseGeoPoint(30d, 50d); object.put("point", point); Parcel parcel = Parcel.obtain(); object.writeToParcel(parcel, 0); parcel.setDataPosition(0); ParseObject newObject = ParseObject.CREATOR.createFromParcel(parcel); assertEquals(newObject.getClassName(), object.getClassName()); assertEquals(newObject.isDeleted, object.isDeleted); assertEquals(newObject.hasChanges(), object.hasChanges()); assertEquals(newObject.getLong("long"), object.getLong("long")); assertEquals(newObject.getDouble("double"), object.getDouble("double"), 0); assertEquals(newObject.getInt("int"), object.getInt("int")); assertEquals(newObject.getString("string"), object.getString("string")); assertEquals(newObject.getDate("date"), object.getDate("date")); assertEquals(newObject.get("null"), object.get("null")); assertEquals(newObject.getList("collection"), object.getList("collection")); assertEquals(newObject.getParseObject("pointer").getClassName(), other.getClassName()); assertEquals(newObject.getParseObject("pointer").getObjectId(), other.getObjectId()); assertEquals(newObject.getMap("map"), object.getMap("map")); assertEquals(newObject.getBytes("bytes").length, bytes.length); assertEquals(newObject.getACL().getReadAccess("reader"), acl.getReadAccess("reader")); ParseRelation newRel = newObject.getRelation("relation"); assertEquals(newRel.getKey(), rel.getKey()); assertEquals(newRel.getKnownObjects().size(), rel.getKnownObjects().size()); newRel.hasKnownObject(related); assertEquals(newObject.getParseFile("file").getUrl(), object.getParseFile("file").getUrl()); assertEquals(newObject.getParseGeoPoint("point").getLatitude(), object.getParseGeoPoint("point").getLatitude(), 0); } @Test public void testParcelWithCircularReference() throws Exception { ParseObject parent = new ParseObject("Parent"); ParseObject child = new ParseObject("Child"); parent.setObjectId("parentId"); parent.put("self", parent); child.setObjectId("childId"); child.put("self", child); child.put("parent", parent); parent.put("child", child); Parcel parcel = Parcel.obtain(); parent.writeToParcel(parcel, 0); parcel.setDataPosition(0); parent = ParseObject.CREATOR.createFromParcel(parcel); assertEquals(parent.getObjectId(), "parentId"); assertEquals(parent.getParseObject("self").getObjectId(), "parentId"); child = parent.getParseObject("child"); assertEquals(child.getObjectId(), "childId"); assertEquals(child.getParseObject("self").getObjectId(), "childId"); assertEquals(child.getParseObject("parent").getObjectId(), "parentId"); } @Test public void testParcelWithCircularReferenceFromServer() throws Exception { ParseObject parent = new ParseObject("Parent"); ParseObject child = new ParseObject("Child"); parent.setState(new ParseObject.State.Builder("Parent") .objectId("parentId") .put("self", parent) .put("child", child).build()); parent.setObjectId("parentId"); child.setState(new ParseObject.State.Builder("Child") .objectId("childId") .put("self", child) .put("parent", parent).build()); Parcel parcel = Parcel.obtain(); parent.writeToParcel(parcel, 0); parcel.setDataPosition(0); parent = ParseObject.CREATOR.createFromParcel(parcel); assertEquals(parent.getObjectId(), "parentId"); assertEquals(parent.getParseObject("self").getObjectId(), "parentId"); child = parent.getParseObject("child"); assertEquals(child.getObjectId(), "childId"); assertEquals(child.getParseObject("self").getObjectId(), "childId"); assertEquals(child.getParseObject("parent").getObjectId(), "parentId"); } @Test public void testParcelWhileSaving() throws Exception { mockCurrentUserController(); TaskCompletionSource<ParseObject.State> tcs = mockObjectControllerForSave(); // Create multiple ParseOperationSets List<Task<Void>> tasks = new ArrayList<>(); ParseObject object = new ParseObject("TestObject"); object.setObjectId("id"); object.put("key", "value"); object.put("number", 5); tasks.add(object.saveInBackground()); object.put("key", "newValue"); object.increment("number", 6); tasks.add(object.saveInBackground()); object.increment("number", -1); tasks.add(object.saveInBackground()); // Ensure Log.w is called... assertTrue(object.hasOutstandingOperations()); Parcel parcel = Parcel.obtain(); object.writeToParcel(parcel, 0); parcel.setDataPosition(0); ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); assertTrue(other.isDirty("key")); assertTrue(other.isDirty("number")); assertEquals(other.getString("key"), "newValue"); assertEquals(other.getNumber("number"), 10); // By design, when LDS is off, we assume that old operations failed even if // they are still running on the old instance. assertFalse(other.hasOutstandingOperations()); // Force finish save operations on the old instance. tcs.setResult(null); ParseTaskUtils.wait(Task.whenAll(tasks)); } @Test public void testParcelWhileSavingWithLDSEnabled() throws Exception { mockCurrentUserController(); TaskCompletionSource<ParseObject.State> tcs = mockObjectControllerForSave(); ParseObject object = new ParseObject("TestObject"); object.setObjectId("id"); OfflineStore lds = mock(OfflineStore.class); when(lds.getObject("TestObject", "id")).thenReturn(object); Parse.setLocalDatastore(lds); object.put("key", "value"); object.increment("number", 3); Task<Void> saveTask = object.saveInBackground(); assertTrue(object.hasOutstandingOperations()); // Saving assertFalse(object.isDirty()); // Not dirty because it's saving Parcel parcel = Parcel.obtain(); object.writeToParcel(parcel, 0); parcel.setDataPosition(0); ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); assertSame(object, other); assertTrue(other.hasOutstandingOperations()); // Still saving assertFalse(other.isDirty()); // Still not dirty assertEquals(other.getNumber("number"), 3); tcs.setResult(null); saveTask.waitForCompletion(); Parse.setLocalDatastore(null); } @Test public void testParcelWhileDeleting() throws Exception { mockCurrentUserController(); TaskCompletionSource<Void> tcs = mockObjectControllerForDelete(); ParseObject object = new ParseObject("TestObject"); object.setObjectId("id"); Task<Void> deleteTask = object.deleteInBackground(); // ensure Log.w is called.. assertTrue(object.isDeleting); Parcel parcel = Parcel.obtain(); object.writeToParcel(parcel, 0); parcel.setDataPosition(0); ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); // By design, when LDS is off, we assume that old operations failed even if // they are still running on the old instance. assertFalse(other.isDeleting); assertTrue(object.isDeleting); tcs.setResult(null); deleteTask.waitForCompletion(); assertFalse(object.isDeleting); assertTrue(object.isDeleted); } @Test public void testParcelWhileDeletingWithLDSEnabled() throws Exception { mockCurrentUserController(); TaskCompletionSource<Void> tcs = mockObjectControllerForDelete(); ParseObject object = new ParseObject("TestObject"); object.setObjectId("id"); OfflineStore lds = mock(OfflineStore.class); when(lds.getObject("TestObject", "id")).thenReturn(object); Parse.setLocalDatastore(lds); Task<Void> deleteTask = object.deleteInBackground(); assertTrue(object.isDeleting); Parcel parcel = Parcel.obtain(); object.writeToParcel(parcel, 0); parcel.setDataPosition(0); ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); assertSame(object, other); assertTrue(other.isDeleting); // Still deleting tcs.setResult(null); deleteTask.waitForCompletion(); // complete deletion on original object. assertFalse(other.isDeleting); assertTrue(other.isDeleted); Parse.setLocalDatastore(null); } //endregion private static void mockCurrentUserController() { ParseCurrentUserController userController = mock(ParseCurrentUserController.class); when(userController.getCurrentSessionTokenAsync()).thenReturn(Task.forResult("token")); when(userController.getAsync()).thenReturn(Task.<ParseUser>forResult(null)); ParseCorePlugins.getInstance().registerCurrentUserController(userController); } // Returns a tcs to control the operation. private static TaskCompletionSource<ParseObject.State> mockObjectControllerForSave() { TaskCompletionSource<ParseObject.State> tcs = new TaskCompletionSource<>(); ParseObjectController objectController = mock(ParseObjectController.class); when(objectController.saveAsync( any(ParseObject.State.class), any(ParseOperationSet.class), anyString(), any(ParseDecoder.class)) ).thenReturn(tcs.getTask()); ParseCorePlugins.getInstance().registerObjectController(objectController); return tcs; } // Returns a tcs to control the operation. private static TaskCompletionSource<Void> mockObjectControllerForDelete() { TaskCompletionSource<Void> tcs = new TaskCompletionSource<>(); ParseObjectController objectController = mock(ParseObjectController.class); when(objectController.deleteAsync( any(ParseObject.State.class), anyString()) ).thenReturn(tcs.getTask()); ParseCorePlugins.getInstance().registerObjectController(objectController); return tcs; } }