mobile/android/tests/background/junit3/src/db/TestBookmarks.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

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)));
  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);
  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.");
  1029     try {
  1030       if (!cur.moveToFirst()) {
  1031         return out;
  1033       final int guidCol = cur.getColumnIndex(BrowserContract.SyncColumns.GUID);
  1034       while (!cur.isAfterLast()) {
  1035         out.add(cur.getString(guidCol));
  1036         cur.moveToNext();
  1038     } finally {
  1039       cur.close();
  1041     return out;
  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.");
  1058     try {
  1059       if (guids == null || guids.length == 0) {
  1060         assertFalse(cur.moveToFirst());
  1061         return;
  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();
  1079       assertEquals(guids.length, i);
  1080     } finally {
  1081       cur.close();
  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 */

mercurial