/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ package org.mozilla.android.sync.test; import java.util.ArrayList; import org.json.simple.JSONArray; import org.mozilla.android.sync.test.helpers.BookmarkHelpers; import org.mozilla.android.sync.test.helpers.ExpectFetchDelegate; import org.mozilla.android.sync.test.helpers.ExpectFetchSinceDelegate; import org.mozilla.android.sync.test.helpers.ExpectFinishDelegate; import org.mozilla.android.sync.test.helpers.ExpectGuidsSinceDelegate; import org.mozilla.android.sync.test.helpers.ExpectInvalidTypeStoreDelegate; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.sync.repositories.InactiveSessionException; import org.mozilla.gecko.sync.repositories.NullCursorException; import org.mozilla.gecko.sync.repositories.RepositorySession; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksDataAccessor; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepositorySession; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepository; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor; import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers; import org.mozilla.gecko.sync.repositories.android.RepoUtils; import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; import org.mozilla.gecko.sync.repositories.domain.Record; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; public class AndroidBrowserBookmarksRepositoryTest extends AndroidBrowserRepositoryTest { @Override protected AndroidBrowserRepository getRepository() { /** * Override this chain in order to avoid our test code having to create two * sessions all the time. */ return new AndroidBrowserBookmarksRepository() { @Override protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) { AndroidBrowserBookmarksRepositorySession session; session = new AndroidBrowserBookmarksRepositorySession(this, context) { @Override protected synchronized void trackGUID(String guid) { System.out.println("Ignoring trackGUID call: this is a test!"); } }; delegate.deferredCreationDelegate().onSessionCreated(session); } }; } @Override protected AndroidBrowserRepositoryDataAccessor getDataAccessor() { return new AndroidBrowserBookmarksDataAccessor(getApplicationContext()); } /** * Hook to return an ExpectFetchDelegate, possibly with special GUIDs ignored. */ @Override public ExpectFetchDelegate preparedExpectFetchDelegate(Record[] expected) { ExpectFetchDelegate delegate = new ExpectFetchDelegate(expected); delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet()); return delegate; } /** * Hook to return an ExpectGuidsSinceDelegate expecting only special GUIDs (if there are any). */ public ExpectGuidsSinceDelegate preparedExpectOnlySpecialGuidsSinceDelegate() { ExpectGuidsSinceDelegate delegate = new ExpectGuidsSinceDelegate(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet().toArray(new String[] {})); return delegate; } /** * Hook to return an ExpectGuidsSinceDelegate, possibly with special GUIDs ignored. */ @Override public ExpectGuidsSinceDelegate preparedExpectGuidsSinceDelegate(String[] expected) { ExpectGuidsSinceDelegate delegate = new ExpectGuidsSinceDelegate(expected); delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet()); return delegate; } /** * Hook to return an ExpectFetchSinceDelegate, possibly with special GUIDs ignored. */ public ExpectFetchSinceDelegate preparedExpectFetchSinceDelegate(long timestamp, String[] expected) { ExpectFetchSinceDelegate delegate = new ExpectFetchSinceDelegate(timestamp, expected); delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet()); return delegate; } // NOTE NOTE NOTE // Must store folder before records if we we are checking that the // records returned are the same as those sent in. If the folder isn't stored // first, the returned records won't be identical to those stored because we // aren't able to find the parent name/guid when we do a fetch. If you don't want // to store a folder first, store your record in "mobile" or one of the folders // that always exists. public void testFetchOneWithChildren() { BookmarkRecord folder = BookmarkHelpers.createFolder1(); BookmarkRecord bookmark1 = BookmarkHelpers.createBookmark1(); BookmarkRecord bookmark2 = BookmarkHelpers.createBookmark2(); RepositorySession session = createAndBeginSession(); Record[] records = new Record[] { folder, bookmark1, bookmark2 }; performWait(storeManyRunnable(session, records)); AndroidBrowserRepositoryDataAccessor helper = getDataAccessor(); helper.dumpDB(); closeDataAccessor(helper); String[] guids = new String[] { folder.guid }; Record[] expected = new Record[] { folder }; performWait(fetchRunnable(session, guids, expected)); dispose(session); } @Override public void testFetchAll() { Record[] expected = new Record[3]; expected[0] = BookmarkHelpers.createFolder1(); expected[1] = BookmarkHelpers.createBookmark1(); expected[2] = BookmarkHelpers.createBookmark2(); basicFetchAllTest(expected); } @Override public void testGuidsSinceReturnMultipleRecords() { BookmarkRecord record0 = BookmarkHelpers.createBookmark1(); BookmarkRecord record1 = BookmarkHelpers.createBookmark2(); guidsSinceReturnMultipleRecords(record0, record1); } @Override public void testGuidsSinceReturnNoRecords() { guidsSinceReturnNoRecords(BookmarkHelpers.createBookmarkInMobileFolder1()); } @Override public void testFetchSinceOneRecord() { fetchSinceOneRecord(BookmarkHelpers.createBookmarkInMobileFolder1(), BookmarkHelpers.createBookmarkInMobileFolder2()); } @Override public void testFetchSinceReturnNoRecords() { fetchSinceReturnNoRecords(BookmarkHelpers.createBookmark1()); } @Override public void testFetchOneRecordByGuid() { fetchOneRecordByGuid(BookmarkHelpers.createBookmarkInMobileFolder1(), BookmarkHelpers.createBookmarkInMobileFolder2()); } @Override public void testFetchMultipleRecordsByGuids() { BookmarkRecord record0 = BookmarkHelpers.createFolder1(); BookmarkRecord record1 = BookmarkHelpers.createBookmark1(); BookmarkRecord record2 = BookmarkHelpers.createBookmark2(); fetchMultipleRecordsByGuids(record0, record1, record2); } @Override public void testFetchNoRecordByGuid() { fetchNoRecordByGuid(BookmarkHelpers.createBookmark1()); } @Override public void testWipe() { doWipe(BookmarkHelpers.createBookmarkInMobileFolder1(), BookmarkHelpers.createBookmarkInMobileFolder2()); } @Override public void testStore() { basicStoreTest(BookmarkHelpers.createBookmark1()); } public void testStoreFolder() { basicStoreTest(BookmarkHelpers.createFolder1()); } /** * TODO: 2011-12-24, tests disabled because we no longer fail * a store call if we get an unknown record type. */ /* * Test storing each different type of Bookmark record. * We expect any records with type other than "bookmark" * or "folder" to fail. For now we throw these away. */ /* public void testStoreMicrosummary() { basicStoreFailTest(BookmarkHelpers.createMicrosummary()); } public void testStoreQuery() { basicStoreFailTest(BookmarkHelpers.createQuery()); } public void testStoreLivemark() { basicStoreFailTest(BookmarkHelpers.createLivemark()); } public void testStoreSeparator() { basicStoreFailTest(BookmarkHelpers.createSeparator()); } */ protected void basicStoreFailTest(Record record) { final RepositorySession session = createAndBeginSession(); performWait(storeRunnable(session, record, new ExpectInvalidTypeStoreDelegate())); dispose(session); } /* * Re-parenting tests */ // Insert two records missing parent, then insert their parent. // Make sure they end up with the correct parent on fetch. public void testBasicReparenting() throws InactiveSessionException { Record[] expected = new Record[] { BookmarkHelpers.createBookmark1(), BookmarkHelpers.createBookmark2(), BookmarkHelpers.createFolder1() }; doMultipleFolderReparentingTest(expected); } // Insert 3 folders and 4 bookmarks in different orders // and make sure they come out parented correctly public void testMultipleFolderReparenting1() throws InactiveSessionException { Record[] expected = new Record[] { BookmarkHelpers.createBookmark1(), BookmarkHelpers.createBookmark2(), BookmarkHelpers.createBookmark3(), BookmarkHelpers.createFolder1(), BookmarkHelpers.createBookmark4(), BookmarkHelpers.createFolder3(), BookmarkHelpers.createFolder2(), }; doMultipleFolderReparentingTest(expected); } public void testMultipleFolderReparenting2() throws InactiveSessionException { Record[] expected = new Record[] { BookmarkHelpers.createBookmark1(), BookmarkHelpers.createBookmark2(), BookmarkHelpers.createBookmark3(), BookmarkHelpers.createFolder1(), BookmarkHelpers.createBookmark4(), BookmarkHelpers.createFolder3(), BookmarkHelpers.createFolder2(), }; doMultipleFolderReparentingTest(expected); } public void testMultipleFolderReparenting3() throws InactiveSessionException { Record[] expected = new Record[] { BookmarkHelpers.createBookmark1(), BookmarkHelpers.createBookmark2(), BookmarkHelpers.createBookmark3(), BookmarkHelpers.createFolder1(), BookmarkHelpers.createBookmark4(), BookmarkHelpers.createFolder3(), BookmarkHelpers.createFolder2(), }; doMultipleFolderReparentingTest(expected); } private void doMultipleFolderReparentingTest(Record[] expected) throws InactiveSessionException { final RepositorySession session = createAndBeginSession(); doStore(session, expected); ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected); performWait(fetchAllRunnable(session, delegate)); performWait(finishRunnable(session, new ExpectFinishDelegate())); } /* * Test storing identical records with different guids. * For bookmarks identical is defined by the following fields * being the same: title, uri, type, parentName */ @Override public void testStoreIdenticalExceptGuid() { storeIdenticalExceptGuid(BookmarkHelpers.createBookmarkInMobileFolder1()); } /* * More complicated situation in which we insert a folder * followed by a couple of its children. We then insert * the folder again but with a different guid. Children * must still get correct parent when they are fetched. * Store a record after with the new guid as the parent * and make sure it works as well. */ public void testStoreIdenticalFoldersWithChildren() { final RepositorySession session = createAndBeginSession(); Record record0 = BookmarkHelpers.createFolder1(); // Get timestamp so that the conflicting folder that we store below is newer. // Children won't come back on this fetch since they haven't been stored, so remove them // before our delegate throws a failure. BookmarkRecord rec0 = (BookmarkRecord) record0; rec0.children = new JSONArray(); performWait(storeRunnable(session, record0)); ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { rec0 }); performWait(fetchRunnable(session, new String[] { record0.guid }, timestampDelegate)); AndroidBrowserRepositoryDataAccessor helper = getDataAccessor(); helper.dumpDB(); closeDataAccessor(helper); Record record1 = BookmarkHelpers.createBookmark1(); Record record2 = BookmarkHelpers.createBookmark2(); Record record3 = BookmarkHelpers.createFolder1(); BookmarkRecord bmk3 = (BookmarkRecord) record3; record3.guid = Utils.generateGuid(); record3.lastModified = timestampDelegate.records.get(0).lastModified + 3000; assert(!record0.guid.equals(record3.guid)); // Store an additional record after inserting the duplicate folder // with new GUID. Make sure it comes back as well. Record record4 = BookmarkHelpers.createBookmark3(); BookmarkRecord bmk4 = (BookmarkRecord) record4; bmk4.parentID = bmk3.guid; bmk4.parentName = bmk3.parentName; doStore(session, new Record[] { record1, record2, record3, bmk4 }); BookmarkRecord bmk1 = (BookmarkRecord) record1; bmk1.parentID = record3.guid; BookmarkRecord bmk2 = (BookmarkRecord) record2; bmk2.parentID = record3.guid; Record[] expect = new Record[] { bmk1, bmk2, record3 }; fetchAllRunnable(session, preparedExpectFetchDelegate(expect)); dispose(session); } @Override public void testRemoteNewerTimeStamp() { BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); remoteNewerTimeStamp(local, remote); } @Override public void testLocalNewerTimeStamp() { BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); localNewerTimeStamp(local, remote); } @Override public void testDeleteRemoteNewer() { BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); deleteRemoteNewer(local, remote); } @Override public void testDeleteLocalNewer() { BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); deleteLocalNewer(local, remote); } @Override public void testDeleteRemoteLocalNonexistent() { BookmarkRecord remote = BookmarkHelpers.createBookmark2(); deleteRemoteLocalNonexistent(remote); } @Override public void testCleanMultipleRecords() { cleanMultipleRecords( BookmarkHelpers.createBookmarkInMobileFolder1(), BookmarkHelpers.createBookmarkInMobileFolder2(), BookmarkHelpers.createBookmark1(), BookmarkHelpers.createBookmark2(), BookmarkHelpers.createFolder1()); } public void testBasicPositioning() { final RepositorySession session = createAndBeginSession(); Record[] expected = new Record[] { BookmarkHelpers.createBookmark1(), BookmarkHelpers.createFolder1(), BookmarkHelpers.createBookmark2() }; System.out.println("TEST: Inserting " + expected[0].guid + ", " + expected[1].guid + ", " + expected[2].guid); doStore(session, expected); ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected); performWait(fetchAllRunnable(session, delegate)); int found = 0; boolean foundFolder = false; for (int i = 0; i < delegate.records.size(); i++) { BookmarkRecord rec = (BookmarkRecord) delegate.records.get(i); if (rec.guid.equals(expected[0].guid)) { assertEquals(0, ((BookmarkRecord) delegate.records.get(i)).androidPosition); found++; } else if (rec.guid.equals(expected[2].guid)) { assertEquals(1, ((BookmarkRecord) delegate.records.get(i)).androidPosition); found++; } else if (rec.guid.equals(expected[1].guid)) { foundFolder = true; } else { System.out.println("TEST: found " + rec.guid); } } assertTrue(foundFolder); assertEquals(2, found); dispose(session); } public void testSqlInjectPurgeDeleteAndUpdateByGuid() { // Some setup. RepositorySession session = createAndBeginSession(); AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); ContentValues cv = new ContentValues(); cv.put(BrowserContract.SyncColumns.IS_DELETED, 1); // Create and insert 2 bookmarks, 2nd one is evil (attempts injection). BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); bmk2.guid = "' or '1'='1"; db.insert(bmk1); db.insert(bmk2); // Test 1 - updateByGuid() handles evil bookmarks correctly. db.updateByGuid(bmk2.guid, cv); // Query bookmarks table. Cursor cur = getAllBookmarks(); int numBookmarks = cur.getCount(); // Ensure only the evil bookmark is marked for deletion. try { cur.moveToFirst(); while (!cur.isAfterLast()) { String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1; if (guid.equals(bmk2.guid)) { assertTrue(deleted); } else { assertFalse(deleted); } cur.moveToNext(); } } finally { cur.close(); } // Test 2 - Ensure purgeDelete()'s call to delete() deletes only 1 record. try { db.purgeDeleted(); } catch (NullCursorException e) { e.printStackTrace(); } cur = getAllBookmarks(); int numBookmarksAfterDeletion = cur.getCount(); // Ensure we have only 1 deleted row. assertEquals(numBookmarksAfterDeletion, numBookmarks - 1); // Ensure only the evil bookmark is deleted. try { cur.moveToFirst(); while (!cur.isAfterLast()) { String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1; if (guid.equals(bmk2.guid)) { fail("Evil guid was not deleted!"); } else { assertFalse(deleted); } cur.moveToNext(); } } finally { cur.close(); } dispose(session); } protected Cursor getAllBookmarks() { Context context = getApplicationContext(); Cursor cur = context.getContentResolver().query(BrowserContractHelpers.BOOKMARKS_CONTENT_URI, BrowserContractHelpers.BookmarkColumns, null, null, null); return cur; } public void testSqlInjectFetch() { // Some setup. RepositorySession session = createAndBeginSession(); AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); // Create and insert 4 bookmarks, last one is evil (attempts injection). BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3(); BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4(); bmk4.guid = "' or '1'='1"; db.insert(bmk1); db.insert(bmk2); db.insert(bmk3); db.insert(bmk4); // Perform a fetch. Cursor cur = null; try { cur = db.fetch(new String[] { bmk3.guid, bmk4.guid }); } catch (NullCursorException e1) { e1.printStackTrace(); } // Ensure the correct number (2) of records were fetched and with the correct guids. if (cur == null) { fail("No records were fetched."); } try { if (cur.getCount() != 2) { fail("Wrong number of guids fetched!"); } cur.moveToFirst(); while (!cur.isAfterLast()) { String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); if (!guid.equals(bmk3.guid) && !guid.equals(bmk4.guid)) { fail("Wrong guids were fetched!"); } cur.moveToNext(); } } finally { cur.close(); } dispose(session); } public void testSqlInjectDelete() { // Some setup. RepositorySession session = createAndBeginSession(); AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); // Create and insert 2 bookmarks, 2nd one is evil (attempts injection). BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); bmk2.guid = "' or '1'='1"; db.insert(bmk1); db.insert(bmk2); // Note size of table before delete. Cursor cur = getAllBookmarks(); int numBookmarks = cur.getCount(); db.purgeGuid(bmk2.guid); // Note size of table after delete. cur = getAllBookmarks(); int numBookmarksAfterDelete = cur.getCount(); // Ensure size of table after delete is *only* 1 less. assertEquals(numBookmarksAfterDelete, numBookmarks - 1); try { cur.moveToFirst(); while (!cur.isAfterLast()) { String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); if (guid.equals(bmk2.guid)) { fail("Guid was not deleted!"); } cur.moveToNext(); } } finally { cur.close(); } dispose(session); } /** * Verify that data accessor's bulkInsert actually inserts. * @throws NullCursorException */ public void testBulkInsert() throws NullCursorException { RepositorySession session = createAndBeginSession(); AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); // Have to set androidID of parent manually. Cursor cur = db.fetch(new String[] { "mobile" } ); assertEquals(1, cur.getCount()); cur.moveToFirst(); int mobileAndroidID = RepoUtils.getIntFromCursor(cur, BrowserContract.Bookmarks._ID); BookmarkRecord bookmark1 = BookmarkHelpers.createBookmarkInMobileFolder1(); BookmarkRecord bookmark2 = BookmarkHelpers.createBookmarkInMobileFolder2(); bookmark1.androidParentID = mobileAndroidID; bookmark2.androidParentID = mobileAndroidID; ArrayList<Record> recordList = new ArrayList<Record>(); recordList.add(bookmark1); recordList.add(bookmark2); db.bulkInsert(recordList); String[] guids = new String[] { bookmark1.guid, bookmark2.guid }; Record[] expected = new Record[] { bookmark1, bookmark2 }; performWait(fetchRunnable(session, guids, expected)); dispose(session); } }