PageRenderTime 56ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/test/src/org/mozilla/gecko/reading/test/TestLiveReadingListSynchronizer.java

http://github.com/mozilla-services/android-sync
Java | 520 lines | 418 code | 77 blank | 25 comment | 7 complexity | 2756e544466d0bc47beaffb82fddc119 MD5 | raw file
Possible License(s): MPL-2.0
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. package org.mozilla.gecko.reading.test;
  5. import java.net.URI;
  6. import java.net.URISyntaxException;
  7. import java.util.Collection;
  8. import java.util.concurrent.CountDownLatch;
  9. import org.mozilla.gecko.background.ReadingListConstants;
  10. import org.mozilla.gecko.background.common.PrefsBranch;
  11. import org.mozilla.gecko.background.common.log.Logger;
  12. import org.mozilla.gecko.background.db.CursorDumper;
  13. import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
  14. import org.mozilla.gecko.background.testhelpers.WaitHelper;
  15. import org.mozilla.gecko.background.testhelpers.WaitHelper.InnerError;
  16. import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
  17. import org.mozilla.gecko.reading.ClientMetadata;
  18. import org.mozilla.gecko.reading.ClientReadingListRecord;
  19. import org.mozilla.gecko.reading.LocalReadingListStorage;
  20. import org.mozilla.gecko.reading.ReadingListClient;
  21. import org.mozilla.gecko.reading.ReadingListDeleteDelegate;
  22. import org.mozilla.gecko.reading.ReadingListRecord;
  23. import org.mozilla.gecko.reading.ReadingListRecord.ServerMetadata;
  24. import org.mozilla.gecko.reading.ReadingListRecordDelegate;
  25. import org.mozilla.gecko.reading.ReadingListRecordResponse;
  26. import org.mozilla.gecko.reading.ReadingListRecordUploadDelegate;
  27. import org.mozilla.gecko.reading.ReadingListResponse;
  28. import org.mozilla.gecko.reading.ReadingListStorage;
  29. import org.mozilla.gecko.reading.ReadingListStorageResponse;
  30. import org.mozilla.gecko.reading.ReadingListSynchronizer;
  31. import org.mozilla.gecko.reading.ReadingListSynchronizerDelegate;
  32. import org.mozilla.gecko.reading.ReadingListWipeDelegate;
  33. import org.mozilla.gecko.reading.ServerReadingListRecord;
  34. import org.mozilla.gecko.sync.ExtendedJSONObject;
  35. import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
  36. import org.mozilla.gecko.sync.net.MozResponse;
  37. import android.content.ContentProviderClient;
  38. import android.content.ContentValues;
  39. import android.content.SharedPreferences;
  40. import android.database.Cursor;
  41. import android.net.Uri;
  42. import android.os.RemoteException;
  43. public class TestLiveReadingListSynchronizer extends ReadingListTest {
  44. static final class TestSynchronizerDelegate implements ReadingListSynchronizerDelegate {
  45. private final CountDownLatch latch;
  46. public volatile boolean onDownloadCompleteCalled = false;
  47. public volatile boolean onModifiedUploadCompleteCalled = false;
  48. public volatile boolean onNewItemUploadCompleteCalled = false;
  49. public volatile boolean onStatusUploadCompleteCalled = false;
  50. public volatile boolean onUnableToSyncCalled = false;
  51. public volatile boolean onDeletionsUploadCompleteCalled = false;
  52. public volatile Exception onUnableToSyncException = null;
  53. public TestSynchronizerDelegate(CountDownLatch latch) {
  54. this.latch = latch;
  55. }
  56. @Override
  57. public void onUnableToSync(Exception e) {
  58. Logger.warn(LOG_TAG, "onUnableToSync", e);
  59. onUnableToSyncException = e;
  60. onUnableToSyncCalled = true;
  61. latch.countDown();
  62. }
  63. @Override
  64. public void onStatusUploadComplete(Collection<String> uploaded,
  65. Collection<String> failed) {
  66. onStatusUploadCompleteCalled = true;
  67. }
  68. @Override
  69. public void onNewItemUploadComplete(Collection<String> uploaded,
  70. Collection<String> failed) {
  71. onNewItemUploadCompleteCalled = true;
  72. }
  73. @Override
  74. public void onModifiedUploadComplete() {
  75. onModifiedUploadCompleteCalled = true;
  76. }
  77. @Override
  78. public void onDownloadComplete() {
  79. onDownloadCompleteCalled = true;
  80. }
  81. @Override
  82. public void onComplete() {
  83. latch.countDown();
  84. }
  85. @Override
  86. public void onDeletionsUploadComplete() {
  87. onDeletionsUploadCompleteCalled = true;
  88. }
  89. }
  90. private static final String DEFAULT_SERVICE_URI = ReadingListConstants.DEFAULT_DEV_ENDPOINT;
  91. private static ReadingListClient getTestClient(final String username) throws URISyntaxException, InterruptedException {
  92. return getTestClient(username, false);
  93. }
  94. private static ReadingListClient getTestClient(String username, boolean wiped) throws URISyntaxException, InterruptedException {
  95. final ReadingListClient client = new ReadingListClient(new URI(DEFAULT_SERVICE_URI), new BasicAuthHeaderProvider(username, "nopassword"));
  96. if (wiped) {
  97. final CountDownLatch latch = new CountDownLatch(1);
  98. ReadingListWipeDelegate delegate = new ReadingListWipeDelegate() {
  99. @Override
  100. public void onSuccess(ReadingListStorageResponse response) {
  101. Logger.info(LOG_TAG, "Got wipe success.");
  102. latch.countDown();
  103. }
  104. @Override
  105. public void onPreconditionFailed(MozResponse response) {
  106. // Should never occur.
  107. fail();
  108. latch.countDown();
  109. }
  110. @Override
  111. public void onFailure(MozResponse response) {
  112. Logger.error(LOG_TAG, "Wipe failed: " + response.getStatusCode());
  113. // Oh well.
  114. fail();
  115. latch.countDown();
  116. }
  117. @Override
  118. public void onFailure(Exception e) {
  119. Logger.error(LOG_TAG, "Wipe failed: " + e);
  120. // Oh well.
  121. fail();
  122. latch.countDown();
  123. }
  124. };
  125. client.wipe(delegate);
  126. latch.await();
  127. }
  128. return client;
  129. }
  130. public final void testBlankSync() throws Exception {
  131. final ContentProviderClient cpc = getWipedLocalClient();
  132. try {
  133. final ReadingListStorage local = new LocalReadingListStorage(cpc);
  134. final SharedPreferences prefs = new MockSharedPreferences();
  135. final ReadingListClient remote = getTestClient("test_android_blank");
  136. final PrefsBranch branch = new PrefsBranch(prefs, "foo.");
  137. final ReadingListSynchronizer synchronizer = new ReadingListSynchronizer(branch, remote, local);
  138. assertFalse(prefs.contains("foo." + ReadingListSynchronizer.PREF_LAST_MODIFIED));
  139. assertFalse(branch.contains(ReadingListSynchronizer.PREF_LAST_MODIFIED));
  140. assertSuccessfulSync(synchronizer);
  141. // We should have a new LM in prefs.
  142. assertTrue(prefs.contains("foo." + ReadingListSynchronizer.PREF_LAST_MODIFIED));
  143. assertTrue(branch.contains(ReadingListSynchronizer.PREF_LAST_MODIFIED));
  144. assertTrue(branch.getLong(ReadingListSynchronizer.PREF_LAST_MODIFIED, -1L) > 1425428783535L);
  145. } finally {
  146. cpc.release();
  147. }
  148. }
  149. public final void testNewUp() throws Exception {
  150. final ContentProviderClient cpc = getWipedLocalClient();
  151. try {
  152. final ReadingListStorage local = new LocalReadingListStorage(cpc);
  153. final SharedPreferences prefs = new MockSharedPreferences();
  154. final ReadingListClient remote = getTestClient("test_android_new_up_2", true);
  155. final PrefsBranch branch = new PrefsBranch(prefs, "foo_new.");
  156. final ReadingListSynchronizer synchronizer = new ReadingListSynchronizer(branch, remote, local);
  157. // Populate a record.
  158. final ContentValues values = new ContentValues();
  159. values.put("url", "http://example.org/reading");
  160. values.put("title", "Example Reading");
  161. values.put("content_status", ReadingListItems.STATUS_FETCH_FAILED_PERMANENT); // So that Gecko won't fetch!
  162. cpc.insert(CONTENT_URI, values);
  163. assertCursorCount(1, local.getNew());
  164. assertCursorCount(0, local.getModified());
  165. assertCursorCount(0, local.getStatusChanges());
  166. assertCursorCount(1, local.getAll());
  167. assertSuccessfulSync(synchronizer);
  168. CursorDumper.dumpCursor(local.getNew());
  169. assertCursorCount(0, local.getNew());
  170. assertCursorCount(0, local.getModified());
  171. assertCursorCount(0, local.getStatusChanges());
  172. assertCursorCount(1, local.getAll());
  173. // Now we applied the remote record, and we can see the changes.
  174. Cursor c = cpc.query(CONTENT_URI_IS_SYNC, null, null, null, null);
  175. String guid = null;
  176. try {
  177. final int colContentStatus = c.getColumnIndexOrThrow(ReadingListItems.CONTENT_STATUS);
  178. final int colTitle = c.getColumnIndexOrThrow(ReadingListItems.TITLE);
  179. final int colGUID = c.getColumnIndexOrThrow(ReadingListItems.GUID);
  180. final int colServerLastModified = c.getColumnIndexOrThrow(ReadingListItems.SERVER_LAST_MODIFIED);
  181. assertTrue(c.moveToFirst());
  182. assertEquals(1, c.getCount());
  183. assertEquals(ReadingListItems.STATUS_FETCH_FAILED_PERMANENT, c.getInt(colContentStatus));
  184. assertEquals("Example Reading", c.getString(colTitle));
  185. assertFalse(c.isNull(colGUID));
  186. guid = c.getString(colGUID);
  187. assertTrue(0 < c.getLong(colServerLastModified));
  188. } finally {
  189. c.close();
  190. }
  191. // Now delete the record locally as Fennec would. The deletion should be uploaded during a sync.
  192. final Uri uri = ReadingListItems.CONTENT_URI;
  193. int deleted = cpc.delete(uri, ReadingListItems.GUID + " = ?", new String[] { guid });
  194. assertEquals(1, deleted);
  195. final Cursor deletedCursor = local.getDeletedItems();
  196. try {
  197. assertEquals(1, deletedCursor.getCount());
  198. deletedCursor.moveToFirst();
  199. assertEquals(guid, deletedCursor.getString(0));
  200. } finally {
  201. deletedCursor.close();
  202. }
  203. assertSuccessfulSync(synchronizer);
  204. assertCursorCount(0, local.getNew());
  205. assertCursorCount(0, local.getModified());
  206. assertCursorCount(0, local.getStatusChanges());
  207. assertCursorCount(0, local.getAll());
  208. // Now touch the server to verify that the item is missing.
  209. try {
  210. getOne(remote, guid);
  211. fail("Should be 404.");
  212. } catch (InnerError e) {
  213. assertEquals("onRecordMissingOrDeleted", e.innerError.getMessage());
  214. }
  215. if (guid != null) {
  216. blindWipe(remote, guid);
  217. }
  218. } finally {
  219. cpc.release();
  220. }
  221. }
  222. protected TestSynchronizerDelegate assertSuccessfulSync(ReadingListSynchronizer synchronizer) throws InterruptedException {
  223. final CountDownLatch latch = new CountDownLatch(1);
  224. final TestSynchronizerDelegate delegate = new TestSynchronizerDelegate(latch);
  225. synchronizer.syncAll(delegate);
  226. latch.await();
  227. if (delegate.onUnableToSyncException != null) {
  228. throw new RuntimeException(delegate.onUnableToSyncException);
  229. }
  230. assertFalse(delegate.onUnableToSyncCalled);
  231. assertTrue(delegate.onDownloadCompleteCalled);
  232. assertTrue(delegate.onModifiedUploadCompleteCalled);
  233. assertTrue(delegate.onStatusUploadCompleteCalled);
  234. assertTrue(delegate.onDeletionsUploadCompleteCalled);
  235. assertTrue(delegate.onNewItemUploadCompleteCalled);
  236. return delegate;
  237. }
  238. public final void testUploadModified() throws Exception {
  239. final ContentProviderClient cpc = getWipedLocalClient();
  240. try {
  241. final ReadingListStorage local = new LocalReadingListStorage(cpc);
  242. final SharedPreferences prefs = new MockSharedPreferences();
  243. final ReadingListClient remote = getTestClient("test_android_upload_modified", true);
  244. final PrefsBranch branch = new PrefsBranch(prefs, "foo_upload_modified.");
  245. final ReadingListSynchronizer synchronizer = new ReadingListSynchronizer(branch, remote, local);
  246. // Populate a record.
  247. final ContentValues values = new ContentValues();
  248. values.put("url", "http://example.org/reading");
  249. values.put("title", "Example Reading");
  250. values.put("content_status", ReadingListItems.STATUS_FETCH_FAILED_PERMANENT); // So that Gecko won't fetch!
  251. cpc.insert(CONTENT_URI, values);
  252. assertCursorCount(1, local.getNew());
  253. assertCursorCount(0, local.getModified());
  254. assertCursorCount(0, local.getStatusChanges());
  255. assertCursorCount(1, local.getAll());
  256. Logger.info(LOG_TAG, "Uploading new item.");
  257. assertSuccessfulSync(synchronizer);
  258. assertCursorCount(0, local.getNew());
  259. assertCursorCount(0, local.getModified());
  260. assertCursorCount(0, local.getStatusChanges());
  261. assertCursorCount(1, local.getAll());
  262. Cursor c = cpc.query(CONTENT_URI_IS_SYNC, null, null, null, null);
  263. String guid = null;
  264. try {
  265. final int colGUID = c.getColumnIndexOrThrow(ReadingListItems.GUID);
  266. assertEquals(1, c.getCount());
  267. assertTrue(c.moveToFirst());
  268. assertFalse(c.isNull(colGUID));
  269. guid = c.getString(colGUID);
  270. } finally {
  271. c.close();
  272. }
  273. Logger.info(LOG_TAG, "Uploaded item was assigned GUID: " + guid);
  274. assertEquals("", getExcerpt(cpc, guid)); // Why not null?
  275. // We should have applied the remote record. Let's make a material change locally.
  276. final String TEST_EXCERPT = "Example reading list excerpt.";
  277. final ContentValues w = new ContentValues();
  278. w.put(ReadingListItems.EXCERPT, TEST_EXCERPT);
  279. assertEquals(1, cpc.update(CONTENT_URI, w, "guid = ?", new String[] { guid }));
  280. Logger.info(LOG_TAG, "Uploading item with material change.");
  281. assertSuccessfulSync(synchronizer);
  282. // We should have no changes remaining to upload.
  283. assertCursorCount(0, local.getNew());
  284. assertCursorCount(0, local.getModified());
  285. assertCursorCount(0, local.getStatusChanges());
  286. assertCursorCount(1, local.getAll());
  287. // Fetch the remote record and verify that our change arrived.
  288. final ServerReadingListRecord record = getOne(remote, guid);
  289. assertNotNull(record);
  290. assertNotNull(record.getExcerpt());
  291. assertEquals(TEST_EXCERPT, record.getExcerpt());
  292. // Now modify the remote record to generate a conflict locally.
  293. final String TEST_PATCHED_EXCERPT = TEST_EXCERPT + " CHANGED";
  294. final ExtendedJSONObject o = new ExtendedJSONObject();
  295. o.put("id", guid);
  296. o.put("excerpt", TEST_PATCHED_EXCERPT);
  297. final ClientMetadata cm = null;
  298. final ServerMetadata sm = new ServerMetadata(guid, -1L);
  299. final ClientReadingListRecord conflictingRecord = new ClientReadingListRecord(sm, cm, o);
  300. final ServerReadingListRecord patchedRecord = patchOne(remote, conflictingRecord);
  301. assertEquals(TEST_PATCHED_EXCERPT, patchedRecord.getExcerpt());
  302. // Now let's make the material change locally again. The server's record
  303. // will obliterate this change; we lose.
  304. assertEquals(1, cpc.update(CONTENT_URI, w, "guid = ?", new String[] { guid }));
  305. assertCursorCount(0, local.getNew());
  306. assertCursorCount(1, local.getModified());
  307. assertCursorCount(0, local.getStatusChanges());
  308. assertCursorCount(1, local.getAll());
  309. Logger.info(LOG_TAG, "Uploading item with conflicting material change.");
  310. assertSuccessfulSync(synchronizer);
  311. // We should have no changes remaining to upload.
  312. assertCursorCount(0, local.getNew());
  313. assertCursorCount(0, local.getModified());
  314. assertCursorCount(0, local.getStatusChanges());
  315. assertCursorCount(1, local.getAll());
  316. // Our local record should have the server's excerpt.
  317. assertEquals(TEST_PATCHED_EXCERPT, getExcerpt(cpc, guid)); // Why not null?
  318. // Fetch the remote record and verify that our change did not make it to the server.
  319. final ServerReadingListRecord fetchedPatchedRecord = getOne(remote, guid);
  320. assertNotNull(fetchedPatchedRecord);
  321. assertNotNull(fetchedPatchedRecord.getExcerpt());
  322. assertEquals(TEST_PATCHED_EXCERPT, fetchedPatchedRecord.getExcerpt());
  323. if (guid != null) {
  324. blindWipe(remote, guid);
  325. }
  326. } finally {
  327. cpc.release();
  328. }
  329. }
  330. private String getExcerpt(ContentProviderClient cpc, String guid) throws RemoteException {
  331. Cursor c = cpc.query(CONTENT_URI_IS_SYNC, null, "guid is ?", new String[] { guid }, null);
  332. try {
  333. final int colExcerpt = c.getColumnIndexOrThrow(ReadingListItems.EXCERPT);
  334. assertEquals(1, c.getCount());
  335. assertTrue(c.moveToFirst());
  336. final String excerpt = c.getString(colExcerpt);
  337. return excerpt;
  338. } finally {
  339. c.close();
  340. }
  341. }
  342. private ServerReadingListRecord getOne(final ReadingListClient remote, final String guid) throws InterruptedException {
  343. final ServerReadingListRecord result[] = new ServerReadingListRecord[1];
  344. WaitHelper.getTestWaiter().performWait(new Runnable() {
  345. @Override
  346. public void run() {
  347. remote.getOne(guid, new ReadingListRecordDelegate() {
  348. @Override
  349. public void onRecordReceived(ServerReadingListRecord record) {
  350. result[0] = record;
  351. WaitHelper.getTestWaiter().performNotify();
  352. }
  353. @Override
  354. public void onRecordMissingOrDeleted(String guid, ReadingListResponse resp) {
  355. WaitHelper.getTestWaiter().performNotify(new RuntimeException("onRecordMissingOrDeleted"));
  356. }
  357. @Override
  358. public void onFailure(Exception error) {
  359. WaitHelper.getTestWaiter().performNotify(error);
  360. }
  361. @Override
  362. public void onFailure(MozResponse response) {
  363. WaitHelper.getTestWaiter().performNotify(new RuntimeException("onFailure"));
  364. }
  365. @Override
  366. public void onComplete(ReadingListResponse response) {
  367. // Ignore -- we should get one of the other callbacks.
  368. }
  369. }, -1);
  370. }
  371. });
  372. return result[0];
  373. }
  374. private ServerReadingListRecord patchOne(final ReadingListClient remote, final ClientReadingListRecord record) throws InterruptedException {
  375. final ServerReadingListRecord result[] = new ServerReadingListRecord[1];
  376. WaitHelper.getTestWaiter().performWait(new Runnable() {
  377. @Override
  378. public void run() {
  379. remote.patch(record, new ReadingListRecordUploadDelegate() {
  380. @Override
  381. public void onSuccess(ClientReadingListRecord up,
  382. ReadingListRecordResponse response,
  383. ServerReadingListRecord down) {
  384. result[0] = down;
  385. WaitHelper.getTestWaiter().performNotify();
  386. }
  387. @Override
  388. public void onInvalidUpload(ClientReadingListRecord up,
  389. ReadingListResponse response) {
  390. WaitHelper.getTestWaiter().performNotify(new RuntimeException("onInvalidUpload"));
  391. }
  392. @Override
  393. public void onFailure(ClientReadingListRecord up, MozResponse response) {
  394. WaitHelper.getTestWaiter().performNotify(new RuntimeException("onFailure"));
  395. }
  396. @Override
  397. public void onFailure(ClientReadingListRecord up, Exception ex) {
  398. WaitHelper.getTestWaiter().performNotify(ex);
  399. }
  400. @Override
  401. public void onConflict(ClientReadingListRecord up,
  402. ReadingListResponse response) {
  403. WaitHelper.getTestWaiter().performNotify(new RuntimeException("onFailure"));
  404. }
  405. @Override
  406. public void onBatchDone() {
  407. // Ignore -- we should get a different callback.
  408. }
  409. @Override
  410. public void onBadRequest(ClientReadingListRecord up, MozResponse response) {
  411. WaitHelper.getTestWaiter().performNotify(new RuntimeException("onFailure"));
  412. }
  413. });
  414. }
  415. });
  416. return result[0];
  417. }
  418. private void blindWipe(final ReadingListClient remote, String guid) {
  419. // Delete it from the server to clean up.
  420. // Eventually we'll have wipe...
  421. remote.delete(guid, new ReadingListDeleteDelegate() {
  422. @Override
  423. public void onSuccess(ReadingListRecordResponse response,
  424. ReadingListRecord record) {
  425. }
  426. @Override
  427. public void onRecordMissingOrDeleted(String guid, MozResponse response) {
  428. }
  429. @Override
  430. public void onPreconditionFailed(String guid, MozResponse response) {
  431. }
  432. @Override
  433. public void onFailure(MozResponse response) {
  434. }
  435. @Override
  436. public void onFailure(Exception e) {
  437. }
  438. @Override
  439. public void onBatchDone() {
  440. }
  441. }, -1L);
  442. }
  443. }