Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* Any copyright is dedicated to the Public Domain.
2 http://creativecommons.org/publicdomain/zero/1.0/ */
4 package org.mozilla.gecko.background.db;
6 import java.util.ArrayList;
7 import java.util.Collection;
8 import java.util.Collections;
9 import java.util.HashSet;
10 import java.util.Iterator;
12 import org.json.simple.JSONArray;
13 import org.mozilla.gecko.R;
14 import org.mozilla.gecko.background.common.log.Logger;
15 import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
16 import org.mozilla.gecko.background.sync.helpers.BookmarkHelpers;
17 import org.mozilla.gecko.background.sync.helpers.SimpleSuccessBeginDelegate;
18 import org.mozilla.gecko.background.sync.helpers.SimpleSuccessCreationDelegate;
19 import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFetchDelegate;
20 import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFinishDelegate;
21 import org.mozilla.gecko.background.sync.helpers.SimpleSuccessStoreDelegate;
22 import org.mozilla.gecko.db.BrowserContract;
23 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
24 import org.mozilla.gecko.sync.Utils;
25 import org.mozilla.gecko.sync.repositories.InactiveSessionException;
26 import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
27 import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
28 import org.mozilla.gecko.sync.repositories.NullCursorException;
29 import org.mozilla.gecko.sync.repositories.RepositorySession;
30 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
31 import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksDataAccessor;
32 import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository;
33 import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepositorySession;
34 import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
35 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
36 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
37 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
38 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate;
39 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
40 import org.mozilla.gecko.sync.repositories.domain.Record;
42 import android.content.ContentResolver;
43 import android.content.ContentUris;
44 import android.content.ContentValues;
45 import android.database.Cursor;
46 import android.net.Uri;
48 public class TestBookmarks extends AndroidSyncTestCase {
50 protected static final String LOG_TAG = "BookmarksTest";
52 /**
53 * Trivial test that forbidden records (reading list prior to Bug 762109, pinned items…)
54 * will be ignored if processed.
55 */
56 public void testForbiddenItemsAreIgnored() {
57 final AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
58 final long now = System.currentTimeMillis();
59 final String bookmarksCollection = "bookmarks";
61 final BookmarkRecord toRead = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now - 1, false);
62 final BookmarkRecord pinned = new BookmarkRecord("pinpinpinpin", "bookmarks", now - 1, false);
63 final BookmarkRecord normal = new BookmarkRecord("baaaaaaaaaaa", "bookmarks", now - 2, false);
65 final BookmarkRecord readingList = new BookmarkRecord(Bookmarks.READING_LIST_FOLDER_GUID,
66 bookmarksCollection, now - 3, false);
67 final BookmarkRecord pinnedItems = new BookmarkRecord(Bookmarks.PINNED_FOLDER_GUID,
68 bookmarksCollection, now - 4, false);
70 toRead.type = normal.type = pinned.type = "bookmark";
71 readingList.type = "folder";
72 pinnedItems.type = "folder";
74 toRead.parentID = Bookmarks.READING_LIST_FOLDER_GUID;
75 pinned.parentID = Bookmarks.PINNED_FOLDER_GUID;
76 normal.parentID = Bookmarks.TOOLBAR_FOLDER_GUID;
78 readingList.parentID = Bookmarks.PLACES_FOLDER_GUID;
79 pinnedItems.parentID = Bookmarks.PLACES_FOLDER_GUID;
81 inBegunSession(repo, new SimpleSuccessBeginDelegate() {
82 @Override
83 public void onBeginSucceeded(RepositorySession session) {
84 assertTrue(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(toRead));
85 assertTrue(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(pinned));
86 assertTrue(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(readingList));
87 assertTrue(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(pinnedItems));
88 assertFalse(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(normal));
89 finishAndNotify(session);
90 }
91 });
92 }
94 /**
95 * Trivial test that pinned items will be skipped if present in the DB.
96 */
97 public void testPinnedItemsAreNotRetrieved() {
98 final AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
100 // Ensure that they exist.
101 setUpFennecPinnedItemsRecord();
103 // They're there in the DB…
104 final ArrayList<String> roots = fetchChildrenDirect(Bookmarks.FIXED_ROOT_ID);
105 Logger.info(LOG_TAG, "Roots: " + roots);
106 assertTrue(roots.contains(Bookmarks.PINNED_FOLDER_GUID));
108 final ArrayList<String> pinned = fetchChildrenDirect(Bookmarks.FIXED_PINNED_LIST_ID);
109 Logger.info(LOG_TAG, "Pinned: " + pinned);
110 assertTrue(pinned.contains("dapinneditem"));
112 // … but not when we fetch.
113 final ArrayList<String> guids = fetchGUIDs(repo);
114 assertFalse(guids.contains(Bookmarks.PINNED_FOLDER_GUID));
115 assertFalse(guids.contains("dapinneditem"));
116 }
118 /**
119 * Trivial test that reading list records will be skipped if present in the DB.
120 */
121 public void testReadingListIsNotRetrieved() {
122 final AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
124 // Ensure that it exists.
125 setUpFennecReadingListRecord();
127 // It's there in the DB…
128 final ArrayList<String> roots = fetchChildrenDirect(BrowserContract.Bookmarks.FIXED_ROOT_ID);
129 Logger.info(LOG_TAG, "Roots: " + roots);
130 assertTrue(roots.contains(Bookmarks.READING_LIST_FOLDER_GUID));
132 // … but not when we fetch.
133 assertFalse(fetchGUIDs(repo).contains(Bookmarks.READING_LIST_FOLDER_GUID));
134 }
136 public void testRetrieveFolderHasAccurateChildren() {
137 AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
139 final long now = System.currentTimeMillis();
141 final String folderGUID = "eaaaaaaaafff";
142 BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now - 5, false);
143 BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now - 1, false);
144 BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now - 3, false);
145 BookmarkRecord bookmarkC = new BookmarkRecord("aaaaaaaaaccc", "bookmarks", now - 2, false);
147 folder.children = childrenFromRecords(bookmarkA, bookmarkB, bookmarkC);
148 folder.sortIndex = 150;
149 folder.title = "Test items";
150 folder.parentID = "toolbar";
151 folder.parentName = "Bookmarks Toolbar";
152 folder.type = "folder";
154 bookmarkA.parentID = folderGUID;
155 bookmarkA.bookmarkURI = "http://example.com/A";
156 bookmarkA.title = "Title A";
157 bookmarkA.type = "bookmark";
159 bookmarkB.parentID = folderGUID;
160 bookmarkB.bookmarkURI = "http://example.com/B";
161 bookmarkB.title = "Title B";
162 bookmarkB.type = "bookmark";
164 bookmarkC.parentID = folderGUID;
165 bookmarkC.bookmarkURI = "http://example.com/C";
166 bookmarkC.title = "Title C";
167 bookmarkC.type = "bookmark";
169 BookmarkRecord[] folderOnly = new BookmarkRecord[1];
170 BookmarkRecord[] children = new BookmarkRecord[3];
172 folderOnly[0] = folder;
174 children[0] = bookmarkA;
175 children[1] = bookmarkB;
176 children[2] = bookmarkC;
178 wipe();
179 Logger.debug(getName(), "Storing just folder...");
180 storeRecordsInSession(repo, folderOnly, null);
182 // We don't have any children, despite our insistence upon storing.
183 assertChildrenAreOrdered(repo, folderGUID, new Record[] {});
185 // Now store the children.
186 Logger.debug(getName(), "Storing children...");
187 storeRecordsInSession(repo, children, null);
189 // Now we have children, but their order is not determined, because
190 // they were stored out-of-session with the original folder.
191 assertChildrenAreUnordered(repo, folderGUID, children);
193 // Now if we store the folder record again, they'll be put in the
194 // right place.
195 folder.lastModified++;
196 Logger.debug(getName(), "Storing just folder again...");
197 storeRecordsInSession(repo, folderOnly, null);
198 Logger.debug(getName(), "Fetching children yet again...");
199 assertChildrenAreOrdered(repo, folderGUID, children);
201 // Now let's see what happens when we see records in the same session.
202 BookmarkRecord[] parentMixed = new BookmarkRecord[4];
203 BookmarkRecord[] parentFirst = new BookmarkRecord[4];
204 BookmarkRecord[] parentLast = new BookmarkRecord[4];
206 // None of our records have a position set.
207 assertTrue(bookmarkA.androidPosition <= 0);
208 assertTrue(bookmarkB.androidPosition <= 0);
209 assertTrue(bookmarkC.androidPosition <= 0);
211 parentMixed[1] = folder;
212 parentMixed[0] = bookmarkA;
213 parentMixed[2] = bookmarkC;
214 parentMixed[3] = bookmarkB;
216 parentFirst[0] = folder;
217 parentFirst[1] = bookmarkC;
218 parentFirst[2] = bookmarkA;
219 parentFirst[3] = bookmarkB;
221 parentLast[3] = folder;
222 parentLast[0] = bookmarkB;
223 parentLast[1] = bookmarkA;
224 parentLast[2] = bookmarkC;
226 wipe();
227 storeRecordsInSession(repo, parentMixed, null);
228 assertChildrenAreOrdered(repo, folderGUID, children);
230 wipe();
231 storeRecordsInSession(repo, parentFirst, null);
232 assertChildrenAreOrdered(repo, folderGUID, children);
234 wipe();
235 storeRecordsInSession(repo, parentLast, null);
236 assertChildrenAreOrdered(repo, folderGUID, children);
238 // Ensure that records are ordered even if we re-process the folder.
239 wipe();
240 storeRecordsInSession(repo, parentLast, null);
241 folder.lastModified++;
242 storeRecordsInSession(repo, folderOnly, null);
243 assertChildrenAreOrdered(repo, folderGUID, children);
244 }
246 public void testMergeFoldersPreservesSaneOrder() {
247 AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
249 final long now = System.currentTimeMillis();
250 final String folderGUID = "mobile";
252 wipe();
253 final long mobile = setUpFennecMobileRecord();
255 // No children.
256 assertChildrenAreUnordered(repo, folderGUID, new Record[] {});
258 // Add some, as Fennec would.
259 fennecAddBookmark("Bookmark One", "http://example.com/fennec/One");
260 fennecAddBookmark("Bookmark Two", "http://example.com/fennec/Two");
262 Logger.debug(getName(), "Fetching children...");
263 JSONArray folderChildren = fetchChildrenForGUID(repo, folderGUID);
265 assertTrue(folderChildren != null);
266 Logger.debug(getName(), "Children are " + folderChildren.toJSONString());
267 assertEquals(2, folderChildren.size());
268 String guidOne = (String) folderChildren.get(0);
269 String guidTwo = (String) folderChildren.get(1);
271 // Make sure positions were saved.
272 assertChildrenAreDirect(mobile, new String[] {
273 guidOne,
274 guidTwo
275 });
277 // Add some through Sync.
278 BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now, false);
279 BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now, false);
280 BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now, false);
282 folder.children = childrenFromRecords(bookmarkA, bookmarkB);
283 folder.sortIndex = 150;
284 folder.title = "Mobile Bookmarks";
285 folder.parentID = "places";
286 folder.parentName = "";
287 folder.type = "folder";
289 bookmarkA.parentID = folderGUID;
290 bookmarkA.parentName = "Mobile Bookmarks"; // Using this title exercises Bug 748898.
291 bookmarkA.bookmarkURI = "http://example.com/A";
292 bookmarkA.title = "Title A";
293 bookmarkA.type = "bookmark";
295 bookmarkB.parentID = folderGUID;
296 bookmarkB.parentName = "mobile";
297 bookmarkB.bookmarkURI = "http://example.com/B";
298 bookmarkB.title = "Title B";
299 bookmarkB.type = "bookmark";
301 BookmarkRecord[] parentMixed = new BookmarkRecord[3];
302 parentMixed[0] = bookmarkA;
303 parentMixed[1] = folder;
304 parentMixed[2] = bookmarkB;
306 storeRecordsInSession(repo, parentMixed, null);
308 BookmarkRecord expectedOne = new BookmarkRecord(guidOne, "bookmarks", now - 10, false);
309 BookmarkRecord expectedTwo = new BookmarkRecord(guidTwo, "bookmarks", now - 10, false);
311 // We want the server to win in this case, and otherwise to preserve order.
312 // TODO
313 assertChildrenAreOrdered(repo, folderGUID, new Record[] {
314 bookmarkA,
315 bookmarkB,
316 expectedOne,
317 expectedTwo
318 });
320 // Furthermore, the children of that folder should be correct in the DB.
321 ContentResolver cr = getApplicationContext().getContentResolver();
322 final long folderId = fennecGetFolderId(cr, folderGUID);
323 Logger.debug(getName(), "Folder " + folderGUID + " => " + folderId);
325 assertChildrenAreDirect(folderId, new String[] {
326 bookmarkA.guid,
327 bookmarkB.guid,
328 expectedOne.guid,
329 expectedTwo.guid
330 });
331 }
333 /**
334 * Apply a folder record whose children array is already accurately
335 * stored in the database. Verify that the parent folder is not flagged
336 * for reupload (i.e., that its modified time is *ahem* unmodified).
337 */
338 public void testNoReorderingMeansNoReupload() {
339 AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
341 final long now = System.currentTimeMillis();
343 final String folderGUID = "eaaaaaaaafff";
344 BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now -5, false);
345 BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now -1, false);
346 BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now -3, false);
348 folder.children = childrenFromRecords(bookmarkA, bookmarkB);
349 folder.sortIndex = 150;
350 folder.title = "Test items";
351 folder.parentID = "toolbar";
352 folder.parentName = "Bookmarks Toolbar";
353 folder.type = "folder";
355 bookmarkA.parentID = folderGUID;
356 bookmarkA.bookmarkURI = "http://example.com/A";
357 bookmarkA.title = "Title A";
358 bookmarkA.type = "bookmark";
360 bookmarkB.parentID = folderGUID;
361 bookmarkB.bookmarkURI = "http://example.com/B";
362 bookmarkB.title = "Title B";
363 bookmarkB.type = "bookmark";
365 BookmarkRecord[] abf = new BookmarkRecord[3];
366 BookmarkRecord[] justFolder = new BookmarkRecord[1];
368 abf[0] = bookmarkA;
369 abf[1] = bookmarkB;
370 abf[2] = folder;
372 justFolder[0] = folder;
374 final String[] abGUIDs = new String[] { bookmarkA.guid, bookmarkB.guid };
375 final Record[] abRecords = new Record[] { bookmarkA, bookmarkB };
376 final String[] baGUIDs = new String[] { bookmarkB.guid, bookmarkA.guid };
377 final Record[] baRecords = new Record[] { bookmarkB, bookmarkA };
379 wipe();
380 Logger.debug(getName(), "Storing A, B, folder...");
381 storeRecordsInSession(repo, abf, null);
383 ContentResolver cr = getApplicationContext().getContentResolver();
384 final long folderID = fennecGetFolderId(cr, folderGUID);
385 assertChildrenAreOrdered(repo, folderGUID, abRecords);
386 assertChildrenAreDirect(folderID, abGUIDs);
388 // To ensure an interval.
389 try {
390 Thread.sleep(100);
391 } catch (InterruptedException e) {
392 }
394 // Store the same folder record again, and check the tracking.
395 // Because the folder array didn't change,
396 // the item is still tracked to not be uploaded.
397 folder.lastModified = System.currentTimeMillis() + 1;
398 HashSet<String> tracked = new HashSet<String>();
399 storeRecordsInSession(repo, justFolder, tracked);
400 assertChildrenAreOrdered(repo, folderGUID, abRecords);
401 assertChildrenAreDirect(folderID, abGUIDs);
403 assertTrue(tracked.contains(folderGUID));
405 // Store again, but with a different order.
406 tracked = new HashSet<String>();
407 folder.children = childrenFromRecords(bookmarkB, bookmarkA);
408 folder.lastModified = System.currentTimeMillis() + 1;
409 storeRecordsInSession(repo, justFolder, tracked);
410 assertChildrenAreOrdered(repo, folderGUID, baRecords);
411 assertChildrenAreDirect(folderID, baGUIDs);
413 // Now it's going to be reuploaded.
414 assertFalse(tracked.contains(folderGUID));
415 }
417 /**
418 * Exercise the deletion of folders when their children have not been
419 * marked as deleted. In a database with constraints, this would fail
420 * if we simply deleted the records, so we move them first.
421 */
422 public void testFolderDeletionOrphansChildren() {
423 AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
425 long now = System.currentTimeMillis();
427 // Add a folder and four children.
428 final String folderGUID = "eaaaaaaaafff";
429 BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now -5, false);
430 BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now -1, false);
431 BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now -3, false);
432 BookmarkRecord bookmarkC = new BookmarkRecord("daaaaaaaaccc", "bookmarks", now -7, false);
433 BookmarkRecord bookmarkD = new BookmarkRecord("baaaaaaaaddd", "bookmarks", now -4, false);
435 folder.children = childrenFromRecords(bookmarkA, bookmarkB, bookmarkC, bookmarkD);
436 folder.sortIndex = 150;
437 folder.title = "Test items";
438 folder.parentID = "toolbar";
439 folder.parentName = "Bookmarks Toolbar";
440 folder.type = "folder";
442 bookmarkA.parentID = folderGUID;
443 bookmarkA.bookmarkURI = "http://example.com/A";
444 bookmarkA.title = "Title A";
445 bookmarkA.type = "bookmark";
447 bookmarkB.parentID = folderGUID;
448 bookmarkB.bookmarkURI = "http://example.com/B";
449 bookmarkB.title = "Title B";
450 bookmarkB.type = "bookmark";
452 bookmarkC.parentID = folderGUID;
453 bookmarkC.bookmarkURI = "http://example.com/C";
454 bookmarkC.title = "Title C";
455 bookmarkC.type = "bookmark";
457 bookmarkD.parentID = folderGUID;
458 bookmarkD.bookmarkURI = "http://example.com/D";
459 bookmarkD.title = "Title D";
460 bookmarkD.type = "bookmark";
462 BookmarkRecord[] abfcd = new BookmarkRecord[5];
463 BookmarkRecord[] justFolder = new BookmarkRecord[1];
464 abfcd[0] = bookmarkA;
465 abfcd[1] = bookmarkB;
466 abfcd[2] = folder;
467 abfcd[3] = bookmarkC;
468 abfcd[4] = bookmarkD;
470 justFolder[0] = folder;
472 final String[] abcdGUIDs = new String[] { bookmarkA.guid, bookmarkB.guid, bookmarkC.guid, bookmarkD.guid };
473 final Record[] abcdRecords = new Record[] { bookmarkA, bookmarkB, bookmarkC, bookmarkD };
475 wipe();
476 Logger.debug(getName(), "Storing A, B, folder, C, D...");
477 storeRecordsInSession(repo, abfcd, null);
479 // Verify that it worked.
480 ContentResolver cr = getApplicationContext().getContentResolver();
481 final long folderID = fennecGetFolderId(cr, folderGUID);
482 assertChildrenAreOrdered(repo, folderGUID, abcdRecords);
483 assertChildrenAreDirect(folderID, abcdGUIDs);
485 now = System.currentTimeMillis();
487 // Add one child to unsorted bookmarks.
488 BookmarkRecord unsortedA = new BookmarkRecord("yiamunsorted", "bookmarks", now, false);
489 unsortedA.parentID = "unfiled";
490 unsortedA.title = "Unsorted A";
491 unsortedA.type = "bookmark";
492 unsortedA.androidPosition = 0;
494 BookmarkRecord[] ua = new BookmarkRecord[1];
495 ua[0] = unsortedA;
497 storeRecordsInSession(repo, ua, null);
499 // Ensure that the database is in this state.
500 assertChildrenAreOrdered(repo, "unfiled", ua);
502 // Delete the second child, the folder, and then the third child.
503 bookmarkB.bookmarkURI = bookmarkC.bookmarkURI = folder.bookmarkURI = null;
504 bookmarkB.deleted = bookmarkC.deleted = folder.deleted = true;
505 bookmarkB.title = bookmarkC.title = folder.title = null;
507 // Nulling the type of folder is very important: it verifies
508 // that the session can behave correctly according to local type.
509 bookmarkB.type = bookmarkC.type = folder.type = null;
511 bookmarkB.lastModified = bookmarkC.lastModified = folder.lastModified = now = System.currentTimeMillis();
513 BookmarkRecord[] deletions = new BookmarkRecord[] { bookmarkB, folder, bookmarkC };
514 storeRecordsInSession(repo, deletions, null);
516 // Verify that the unsorted bookmarks folder contains its child and the
517 // first and fourth children of the now-deleted folder.
518 // Also verify that the folder is gone.
519 long unsortedID = fennecGetFolderId(cr, "unfiled");
520 long toolbarID = fennecGetFolderId(cr, "toolbar");
521 String[] expected = new String[] { unsortedA.guid, bookmarkA.guid, bookmarkD.guid };
523 // This will trigger positioning.
524 assertChildrenAreUnordered(repo, "unfiled", new Record[] { unsortedA, bookmarkA, bookmarkD });
525 assertChildrenAreDirect(unsortedID, expected);
526 assertChildrenAreDirect(toolbarID, new String[] {});
527 }
529 /**
530 * A test where we expect to replace a local folder with a new folder (with a
531 * new GUID), whilst adding children to it. Verifies that replace and insert
532 * co-operate.
533 */
534 public void testInsertAndReplaceGuid() {
535 AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
536 wipe();
538 BookmarkRecord folder1 = BookmarkHelpers.createFolder1();
539 BookmarkRecord folder2 = BookmarkHelpers.createFolder2(); // child of folder1
540 BookmarkRecord folder3 = BookmarkHelpers.createFolder3(); // child of folder2
541 BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); // child of folder1
542 BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); // child of folder1
543 BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3(); // child of folder2
544 BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4(); // child of folder3
546 BookmarkRecord[] records = new BookmarkRecord[] {
547 folder1, folder2, folder3,
548 bmk1, bmk4
549 };
550 storeRecordsInSession(repo, records, null);
552 assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, folder2 });
553 assertChildrenAreUnordered(repo, folder2.guid, new Record[] { folder3 });
554 assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 });
556 // Replace folder3 with a record with a new GUID, and add bmk4 as folder3's child.
557 final long now = System.currentTimeMillis();
558 folder3.guid = Utils.generateGuid();
559 folder3.lastModified = now;
560 bmk4.title = bmk4.title + "/NEW";
561 bmk4.parentID = folder3.guid; // Incoming child knows its parent.
562 bmk4.parentName = folder3.title;
563 bmk4.lastModified = now;
565 // Order of store should not matter.
566 ArrayList<BookmarkRecord> changedRecords = new ArrayList<BookmarkRecord>();
567 changedRecords.add(bmk2); changedRecords.add(bmk3); changedRecords.add(bmk4); changedRecords.add(folder3);
568 Collections.shuffle(changedRecords);
569 storeRecordsInSession(repo, changedRecords.toArray(new BookmarkRecord[changedRecords.size()]), null);
571 assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, bmk2, folder2 });
572 assertChildrenAreUnordered(repo, folder2.guid, new Record[] { bmk3, folder3 });
573 assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 });
575 assertNotNull(fetchGUID(repo, folder3.guid));
576 assertEquals(bmk4.title, fetchGUID(repo, bmk4.guid).title);
577 }
579 /**
580 * A test where we expect to replace a local folder with a new folder (with a
581 * new title but the same GUID), whilst adding children to it. Verifies that
582 * replace and insert co-operate.
583 */
584 public void testInsertAndReplaceTitle() {
585 AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository();
586 wipe();
588 BookmarkRecord folder1 = BookmarkHelpers.createFolder1();
589 BookmarkRecord folder2 = BookmarkHelpers.createFolder2(); // child of folder1
590 BookmarkRecord folder3 = BookmarkHelpers.createFolder3(); // child of folder2
591 BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); // child of folder1
592 BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); // child of folder1
593 BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3(); // child of folder2
594 BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4(); // child of folder3
596 BookmarkRecord[] records = new BookmarkRecord[] {
597 folder1, folder2, folder3,
598 bmk1, bmk4
599 };
600 storeRecordsInSession(repo, records, null);
602 assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, folder2 });
603 assertChildrenAreUnordered(repo, folder2.guid, new Record[] { folder3 });
604 assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 });
606 // Rename folder1, and add bmk2 as folder1's child.
607 final long now = System.currentTimeMillis();
608 folder1.title = folder1.title + "/NEW";
609 folder1.lastModified = now;
610 bmk2.title = bmk2.title + "/NEW";
611 bmk2.parentID = folder1.guid; // Incoming child knows its parent.
612 bmk2.parentName = folder1.title;
613 bmk2.lastModified = now;
615 // Order of store should not matter.
616 ArrayList<BookmarkRecord> changedRecords = new ArrayList<BookmarkRecord>();
617 changedRecords.add(bmk2); changedRecords.add(bmk3); changedRecords.add(folder1);
618 Collections.shuffle(changedRecords);
619 storeRecordsInSession(repo, changedRecords.toArray(new BookmarkRecord[changedRecords.size()]), null);
621 assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, bmk2, folder2 });
622 assertChildrenAreUnordered(repo, folder2.guid, new Record[] { bmk3, folder3 });
623 assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 });
625 assertEquals(folder1.title, fetchGUID(repo, folder1.guid).title);
626 assertEquals(bmk2.title, fetchGUID(repo, bmk2.guid).title);
627 }
629 /**
630 * Create and begin a new session, handing control to the delegate when started.
631 * Returns when the delegate has notified.
632 */
633 public void inBegunSession(final AndroidBrowserBookmarksRepository repo,
634 final RepositorySessionBeginDelegate beginDelegate) {
635 Runnable go = new Runnable() {
636 @Override
637 public void run() {
638 RepositorySessionCreationDelegate delegate = new SimpleSuccessCreationDelegate() {
639 @Override
640 public void onSessionCreated(final RepositorySession session) {
641 try {
642 session.begin(beginDelegate);
643 } catch (InvalidSessionTransitionException e) {
644 performNotify(e);
645 }
646 }
647 };
648 repo.createSession(delegate, getApplicationContext());
649 }
650 };
651 performWait(go);
652 }
654 /**
655 * Finish the provided session, notifying on success.
656 *
657 * @param session
658 */
659 public void finishAndNotify(final RepositorySession session) {
660 try {
661 session.finish(new SimpleSuccessFinishDelegate() {
662 @Override
663 public void onFinishSucceeded(RepositorySession session,
664 RepositorySessionBundle bundle) {
665 performNotify();
666 }
667 });
668 } catch (InactiveSessionException e) {
669 performNotify(e);
670 }
671 }
673 /**
674 * Simple helper class for fetching all records.
675 * The fetched records' GUIDs are stored in `fetchedGUIDs`.
676 */
677 public class SimpleFetchAllBeginDelegate extends SimpleSuccessBeginDelegate {
678 public final ArrayList<String> fetchedGUIDs = new ArrayList<String>();
680 @Override
681 public void onBeginSucceeded(final RepositorySession session) {
682 RepositorySessionFetchRecordsDelegate fetchDelegate = new SimpleSuccessFetchDelegate() {
684 @Override
685 public void onFetchedRecord(Record record) {
686 fetchedGUIDs.add(record.guid);
687 }
689 @Override
690 public void onFetchCompleted(long end) {
691 finishAndNotify(session);
692 }
693 };
694 session.fetchSince(0, fetchDelegate);
695 }
696 }
698 /**
699 * Simple helper class for fetching a single record by GUID.
700 * The fetched record is stored in `fetchedRecord`.
701 */
702 public class SimpleFetchOneBeginDelegate extends SimpleSuccessBeginDelegate {
703 public final String guid;
704 public Record fetchedRecord = null;
706 public SimpleFetchOneBeginDelegate(String guid) {
707 this.guid = guid;
708 }
710 @Override
711 public void onBeginSucceeded(final RepositorySession session) {
712 RepositorySessionFetchRecordsDelegate fetchDelegate = new SimpleSuccessFetchDelegate() {
714 @Override
715 public void onFetchedRecord(Record record) {
716 fetchedRecord = record;
717 }
719 @Override
720 public void onFetchCompleted(long end) {
721 finishAndNotify(session);
722 }
723 };
724 try {
725 session.fetch(new String[] { guid }, fetchDelegate);
726 } catch (InactiveSessionException e) {
727 performNotify("Session is inactive.", e);
728 }
729 }
730 }
732 /**
733 * Create a new session for the given repository, storing each record
734 * from the provided array. Notifies on failure or success.
735 *
736 * Optionally populates a provided Collection with tracked items.
737 * @param repo
738 * @param records
739 * @param tracked
740 */
741 public void storeRecordsInSession(AndroidBrowserBookmarksRepository repo,
742 final BookmarkRecord[] records,
743 final Collection<String> tracked) {
744 SimpleSuccessBeginDelegate beginDelegate = new SimpleSuccessBeginDelegate() {
745 @Override
746 public void onBeginSucceeded(final RepositorySession session) {
747 RepositorySessionStoreDelegate storeDelegate = new SimpleSuccessStoreDelegate() {
749 @Override
750 public void onStoreCompleted(final long storeEnd) {
751 // Pass back whatever we tracked.
752 if (tracked != null) {
753 Iterator<String> iter = session.getTrackedRecordIDs();
754 while (iter.hasNext()) {
755 tracked.add(iter.next());
756 }
757 }
758 finishAndNotify(session);
759 }
761 @Override
762 public void onRecordStoreSucceeded(String guid) {
763 }
764 };
765 session.setStoreDelegate(storeDelegate);
766 for (BookmarkRecord record : records) {
767 try {
768 session.store(record);
769 } catch (NoStoreDelegateException e) {
770 // Never happens.
771 }
772 }
773 session.storeDone();
774 }
775 };
776 inBegunSession(repo, beginDelegate);
777 }
779 public ArrayList<String> fetchGUIDs(AndroidBrowserBookmarksRepository repo) {
780 SimpleFetchAllBeginDelegate beginDelegate = new SimpleFetchAllBeginDelegate();
781 inBegunSession(repo, beginDelegate);
782 return beginDelegate.fetchedGUIDs;
783 }
785 public BookmarkRecord fetchGUID(AndroidBrowserBookmarksRepository repo,
786 final String guid) {
787 Logger.info(LOG_TAG, "Fetching for " + guid);
788 SimpleFetchOneBeginDelegate beginDelegate = new SimpleFetchOneBeginDelegate(guid);
789 inBegunSession(repo, beginDelegate);
790 Logger.info(LOG_TAG, "Fetched " + beginDelegate.fetchedRecord);
791 assertTrue(beginDelegate.fetchedRecord != null);
792 return (BookmarkRecord) beginDelegate.fetchedRecord;
793 }
795 public JSONArray fetchChildrenForGUID(AndroidBrowserBookmarksRepository repo,
796 final String guid) {
797 return fetchGUID(repo, guid).children;
798 }
800 @SuppressWarnings("unchecked")
801 protected static JSONArray childrenFromRecords(BookmarkRecord... records) {
802 JSONArray children = new JSONArray();
803 for (BookmarkRecord record : records) {
804 children.add(record.guid);
805 }
806 return children;
807 }
810 protected void updateRow(ContentValues values) {
811 Uri uri = BrowserContractHelpers.BOOKMARKS_CONTENT_URI;
812 final String where = BrowserContract.Bookmarks.GUID + " = ?";
813 final String[] args = new String[] { values.getAsString(BrowserContract.Bookmarks.GUID) };
814 getApplicationContext().getContentResolver().update(uri, values, where, args);
815 }
817 protected Uri insertRow(ContentValues values) {
818 Uri uri = BrowserContractHelpers.BOOKMARKS_CONTENT_URI;
819 return getApplicationContext().getContentResolver().insert(uri, values);
820 }
822 protected static ContentValues specialFolder() {
823 ContentValues values = new ContentValues();
825 final long now = System.currentTimeMillis();
826 values.put(Bookmarks.DATE_CREATED, now);
827 values.put(Bookmarks.DATE_MODIFIED, now);
828 values.put(Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_FOLDER);
830 return values;
831 }
833 protected static ContentValues fennecMobileRecordWithoutTitle() {
834 ContentValues values = specialFolder();
835 values.put(BrowserContract.SyncColumns.GUID, "mobile");
836 values.putNull(BrowserContract.Bookmarks.TITLE);
838 return values;
839 }
841 protected ContentValues fennecPinnedItemsRecord() {
842 final ContentValues values = specialFolder();
843 final String title = getApplicationContext().getResources().getString(R.string.bookmarks_folder_pinned);
845 values.put(BrowserContract.SyncColumns.GUID, Bookmarks.PINNED_FOLDER_GUID);
846 values.put(Bookmarks._ID, Bookmarks.FIXED_PINNED_LIST_ID);
847 values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
848 values.put(Bookmarks.TITLE, title);
849 return values;
850 }
852 protected static ContentValues fennecPinnedChildItemRecord() {
853 ContentValues values = new ContentValues();
855 final long now = System.currentTimeMillis();
857 values.put(BrowserContract.SyncColumns.GUID, "dapinneditem");
858 values.put(Bookmarks.DATE_CREATED, now);
859 values.put(Bookmarks.DATE_MODIFIED, now);
860 values.put(Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_BOOKMARK);
861 values.put(Bookmarks.URL, "user-entered:foobar");
862 values.put(Bookmarks.PARENT, Bookmarks.FIXED_PINNED_LIST_ID);
863 values.put(Bookmarks.TITLE, "Foobar");
864 return values;
865 }
867 protected ContentValues fennecReadingListRecord() {
868 final ContentValues values = specialFolder();
869 final String title = getApplicationContext().getResources().getString(R.string.bookmarks_folder_reading_list);
871 values.put(BrowserContract.SyncColumns.GUID, Bookmarks.READING_LIST_FOLDER_GUID);
872 values.put(Bookmarks._ID, Bookmarks.FIXED_READING_LIST_ID);
873 values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID);
874 values.put(Bookmarks.TITLE, title);
875 return values;
876 }
878 protected long setUpFennecMobileRecordWithoutTitle() {
879 ContentResolver cr = getApplicationContext().getContentResolver();
880 ContentValues values = fennecMobileRecordWithoutTitle();
881 updateRow(values);
882 return fennecGetMobileBookmarksFolderId(cr);
883 }
885 protected long setUpFennecMobileRecord() {
886 ContentResolver cr = getApplicationContext().getContentResolver();
887 ContentValues values = fennecMobileRecordWithoutTitle();
888 values.put(BrowserContract.Bookmarks.PARENT, BrowserContract.Bookmarks.FIXED_ROOT_ID);
889 String title = getApplicationContext().getResources().getString(R.string.bookmarks_folder_mobile);
890 values.put(BrowserContract.Bookmarks.TITLE, title);
891 updateRow(values);
892 return fennecGetMobileBookmarksFolderId(cr);
893 }
895 protected void setUpFennecPinnedItemsRecord() {
896 insertRow(fennecPinnedItemsRecord());
897 insertRow(fennecPinnedChildItemRecord());
898 }
900 protected void setUpFennecReadingListRecord() {
901 insertRow(fennecReadingListRecord());
902 }
904 //
905 // Fennec fake layer.
906 //
907 private Uri appendProfile(Uri uri) {
908 final String defaultProfile = "default"; // Fennec constant removed in Bug 715307.
909 return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, defaultProfile).build();
910 }
912 private long fennecGetFolderId(ContentResolver cr, String guid) {
913 Cursor c = null;
914 try {
915 c = cr.query(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI),
916 new String[] { BrowserContract.Bookmarks._ID },
917 BrowserContract.Bookmarks.GUID + " = ?",
918 new String[] { guid },
919 null);
921 if (c.moveToFirst()) {
922 return c.getLong(c.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID));
923 }
924 return -1;
925 } finally {
926 if (c != null) {
927 c.close();
928 }
929 }
930 }
932 private long fennecGetMobileBookmarksFolderId(ContentResolver cr) {
933 return fennecGetFolderId(cr, BrowserContract.Bookmarks.MOBILE_FOLDER_GUID);
934 }
936 public void fennecAddBookmark(String title, String uri) {
937 ContentResolver cr = getApplicationContext().getContentResolver();
939 long folderId = fennecGetMobileBookmarksFolderId(cr);
940 if (folderId < 0) {
941 return;
942 }
944 ContentValues values = new ContentValues();
945 values.put(BrowserContract.Bookmarks.TITLE, title);
946 values.put(BrowserContract.Bookmarks.URL, uri);
947 values.put(BrowserContract.Bookmarks.PARENT, folderId);
949 // Restore deleted record if possible
950 values.put(BrowserContract.Bookmarks.IS_DELETED, 0);
952 Logger.debug(getName(), "Adding bookmark " + title + ", " + uri + " in " + folderId);
953 int updated = cr.update(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI),
954 values,
955 BrowserContract.Bookmarks.URL + " = ?",
956 new String[] { uri });
958 if (updated == 0) {
959 Uri insert = cr.insert(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI), values);
960 long idFromUri = ContentUris.parseId(insert);
961 Logger.debug(getName(), "Inserted " + uri + " as " + idFromUri);
962 Logger.debug(getName(), "Position is " + getPosition(idFromUri));
963 }
964 }
966 private long getPosition(long idFromUri) {
967 ContentResolver cr = getApplicationContext().getContentResolver();
968 Cursor c = cr.query(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI),
969 new String[] { BrowserContract.Bookmarks.POSITION },
970 BrowserContract.Bookmarks._ID + " = ?",
971 new String[] { String.valueOf(idFromUri) },
972 null);
973 if (!c.moveToFirst()) {
974 return -2;
975 }
976 return c.getLong(0);
977 }
979 protected AndroidBrowserBookmarksDataAccessor dataAccessor = null;
980 protected AndroidBrowserBookmarksDataAccessor getDataAccessor() {
981 if (dataAccessor == null) {
982 dataAccessor = new AndroidBrowserBookmarksDataAccessor(getApplicationContext());
983 }
984 return dataAccessor;
985 }
987 protected void wipe() {
988 Logger.debug(getName(), "Wiping.");
989 getDataAccessor().wipe();
990 }
992 protected void assertChildrenAreOrdered(AndroidBrowserBookmarksRepository repo, String guid, Record[] expected) {
993 Logger.debug(getName(), "Fetching children...");
994 JSONArray folderChildren = fetchChildrenForGUID(repo, guid);
996 assertTrue(folderChildren != null);
997 Logger.debug(getName(), "Children are " + folderChildren.toJSONString());
998 assertEquals(expected.length, folderChildren.size());
999 for (int i = 0; i < expected.length; ++i) {
1000 assertEquals(expected[i].guid, ((String) folderChildren.get(i)));
1001 }
1002 }
1004 protected void assertChildrenAreUnordered(AndroidBrowserBookmarksRepository repo, String guid, Record[] expected) {
1005 Logger.debug(getName(), "Fetching children...");
1006 JSONArray folderChildren = fetchChildrenForGUID(repo, guid);
1008 assertTrue(folderChildren != null);
1009 Logger.debug(getName(), "Children are " + folderChildren.toJSONString());
1010 assertEquals(expected.length, folderChildren.size());
1011 for (Record record : expected) {
1012 folderChildren.contains(record.guid);
1013 }
1014 }
1016 /**
1017 * Return a sequence of children GUIDs for the provided folder ID.
1018 */
1019 protected ArrayList<String> fetchChildrenDirect(long id) {
1020 Logger.debug(getName(), "Fetching children directly from DB...");
1021 final ArrayList<String> out = new ArrayList<String>();
1022 final AndroidBrowserBookmarksDataAccessor accessor = new AndroidBrowserBookmarksDataAccessor(getApplicationContext());
1023 Cursor cur = null;
1024 try {
1025 cur = accessor.getChildren(id);
1026 } catch (NullCursorException e) {
1027 fail("Got null cursor.");
1028 }
1029 try {
1030 if (!cur.moveToFirst()) {
1031 return out;
1032 }
1033 final int guidCol = cur.getColumnIndex(BrowserContract.SyncColumns.GUID);
1034 while (!cur.isAfterLast()) {
1035 out.add(cur.getString(guidCol));
1036 cur.moveToNext();
1037 }
1038 } finally {
1039 cur.close();
1040 }
1041 return out;
1042 }
1044 /**
1045 * Assert that the children of the provided ID are correct and positioned in the database.
1046 * @param id
1047 * @param guids
1048 */
1049 protected void assertChildrenAreDirect(long id, String[] guids) {
1050 Logger.debug(getName(), "Fetching children directly from DB...");
1051 AndroidBrowserBookmarksDataAccessor accessor = new AndroidBrowserBookmarksDataAccessor(getApplicationContext());
1052 Cursor cur = null;
1053 try {
1054 cur = accessor.getChildren(id);
1055 } catch (NullCursorException e) {
1056 fail("Got null cursor.");
1057 }
1058 try {
1059 if (guids == null || guids.length == 0) {
1060 assertFalse(cur.moveToFirst());
1061 return;
1062 }
1064 assertTrue(cur.moveToFirst());
1065 int i = 0;
1066 final int guidCol = cur.getColumnIndex(BrowserContract.SyncColumns.GUID);
1067 final int posCol = cur.getColumnIndex(BrowserContract.Bookmarks.POSITION);
1068 while (!cur.isAfterLast()) {
1069 assertTrue(i < guids.length);
1070 final String guid = cur.getString(guidCol);
1071 final int pos = cur.getInt(posCol);
1072 Logger.debug(getName(), "Fetched child: " + guid + " has position " + pos);
1073 assertEquals(guids[i], guid);
1074 assertEquals(i, pos);
1076 ++i;
1077 cur.moveToNext();
1078 }
1079 assertEquals(guids.length, i);
1080 } finally {
1081 cur.close();
1082 }
1083 }
1084 }
1086 /**
1087 TODO
1089 Test for storing a record that will reconcile to mobile; postcondition is
1090 that there's still a directory called mobile that includes all the items that
1091 it used to.
1093 mobile folder created without title.
1094 Unsorted put in mobile???
1095 Tests for children retrieval
1096 Tests for children merge
1097 Tests for modify retrieve parent when child added, removed, reordered (oh, reorder is hard! Any change, then.)
1098 Safety mode?
1099 Test storing folder first, contents first.
1100 Store folder in next session. Verify order recovery.
1103 */