diff -r 000000000000 -r 6474c204b198 mobile/android/base/tests/testReadingListProvider.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/tests/testReadingListProvider.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,558 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.tests; + +import java.util.HashSet; +import java.util.Random; +import java.util.concurrent.Callable; + +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserContract.ReadingListItems; +import org.mozilla.gecko.db.ReadingListProvider; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +public class testReadingListProvider extends ContentProviderTest { + + private static final String DB_NAME = "browser.db"; + + // List of tests to be run sorted by dependency. + private final TestCase[] TESTS_TO_RUN = { new TestInsertItems(), + new TestDeleteItems(), + new TestUpdateItems(), + new TestBatchOperations(), + new TestBrowserProviderNotifications() }; + + // Columns used to test for item equivalence. + final String[] TEST_COLUMNS = { ReadingListItems.TITLE, + ReadingListItems.URL, + ReadingListItems.EXCERPT, + ReadingListItems.LENGTH, + ReadingListItems.DATE_CREATED }; + + // Indicates that insertions have been tested. ContentProvider.insert + // has been proven to work. + private boolean mContentProviderInsertTested = false; + + // Indicates that updates have been tested. ContentProvider.update + // has been proven to work. + private boolean mContentProviderUpdateTested = false; + + /** + * Factory function that makes new ReadingListProvider instances. + *

+ * We want a fresh provider each test, so this should be invoked in + * setUp before each individual test. + */ + private static Callable sProviderFactory = new Callable() { + @Override + public ContentProvider call() { + return new ReadingListProvider(); + } + }; + + @Override + public void setUp() throws Exception { + super.setUp(sProviderFactory, BrowserContract.READING_LIST_AUTHORITY, DB_NAME); + for (TestCase test: TESTS_TO_RUN) { + mTests.add(test); + } + } + + public void testReadingListProviderTests() throws Exception { + for (Runnable test : mTests) { + setTestName(test.getClass().getSimpleName()); + ensureEmptyDatabase(); + test.run(); + } + + // Ensure browser initialization is complete before completing test, + // so that the minidumps directory is consistently created. + blockForGeckoReady(); + } + + /** + * Verify that we can insert a reading list item into the DB. + */ + private class TestInsertItems extends TestCase { + @Override + public void test() throws Exception { + ContentValues b = createFillerReadingListItem(); + long id = ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, b)); + Cursor c = getItemById(id); + + try { + mAsserter.ok(c.moveToFirst(), "Inserted item found", ""); + assertRowEqualsContentValues(c, b); + } finally { + c.close(); + } + + testInsertWithNullCol(ReadingListItems.GUID); + mContentProviderInsertTested = true; + } + + /** + * Test that insertion fails when a required column + * is null. + */ + private void testInsertWithNullCol(String colName) { + ContentValues b = createFillerReadingListItem(); + b.putNull(colName); + + try { + ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, b)); + // If we get to here, the flawed insertion succeeded. Fail the test. + mAsserter.ok(false, "Insertion did not succeed with " + colName + " == null", ""); + } catch (NullPointerException e) { + // Indicates test was successful. + } + } + } + + /** + * Verify that we can remove a reading list item from the DB. + */ + private class TestDeleteItems extends TestCase { + + @Override + public void test() throws Exception { + long id = insertAnItemWithAssertion(); + // Test that the item is only marked as deleted and + // not removed from the database. + testNonFirefoxSyncDelete(id); + + // Test that the item is removed from the database. + testFirefoxSyncDelete(id); + + id = insertAnItemWithAssertion(); + // Test that deleting works with only a URI. + testDeleteWithItemURI(id); + } + + /** + * Delete an item with PARAM_IS_SYNC unset and verify that item was only marked + * as deleted and not actually removed from the database. Also verify that the item + * marked as deleted doesn't show up in a query. + * + * @param id of the item to be deleted + */ + private void testNonFirefoxSyncDelete(long id) { + final int deleted = mProvider.delete(ReadingListItems.CONTENT_URI, + ReadingListItems._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is(deleted, 1, "Inserted item was deleted"); + + // PARAM_SHOW_DELETED in the URI allows items marked as deleted to be + // included in the query. + Uri uri = appendUriParam(ReadingListItems.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"); + assertItemExistsByID(uri, id, "Deleted item was only marked as deleted"); + + // Test that the 'deleted' item does not show up in a query when PARAM_SHOW_DELETED + // is not specified in the URI. + assertItemDoesNotExistByID(id, "Inserted item can't be found after deletion"); + } + + /** + * Delete an item with PARAM_IS_SYNC=1 and verify that item + * was actually removed from the database. + * + * @param id of the item to be deleted + */ + private void testFirefoxSyncDelete(long id) { + final int deleted = mProvider.delete(appendUriParam(ReadingListItems.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), + ReadingListItems._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is(deleted, 1, "Inserted item was deleted"); + + Uri uri = appendUriParam(ReadingListItems.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"); + assertItemDoesNotExistByID(uri, id, "Inserted item is now actually deleted"); + } + + /** + * Delete an item with its URI and verify that the item + * was actually removed from the database. + * + * @param id of the item to be deleted + */ + private void testDeleteWithItemURI(long id) { + final int deleted = mProvider.delete(ContentUris.withAppendedId(ReadingListItems.CONTENT_URI, id), null, null); + mAsserter.is(deleted, 1, "Inserted item was deleted using URI with id"); + } + } + + /** + * Verify that we can update reading list items. + */ + private class TestUpdateItems extends TestCase { + + @Override + public void test() throws Exception { + // We should be able to insert into the DB. + ensureCanInsert(); + + ContentValues original = createFillerReadingListItem(); + long id = ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, original)); + int updated = 0; + Long originalDateCreated = null; + Long originalDateModified = null; + ContentValues updates = new ContentValues(); + Cursor c = getItemById(id); + try { + mAsserter.ok(c.moveToFirst(), "Inserted item found", ""); + + originalDateCreated = c.getLong(c.getColumnIndex(ReadingListItems.DATE_CREATED)); + originalDateModified = c.getLong(c.getColumnIndex(ReadingListItems.DATE_MODIFIED)); + + updates.put(ReadingListItems.TITLE, original.getAsString(ReadingListItems.TITLE) + "CHANGED"); + updates.put(ReadingListItems.URL, original.getAsString(ReadingListItems.URL) + "/more/stuff"); + updates.put(ReadingListItems.EXCERPT, original.getAsString(ReadingListItems.EXCERPT) + "CHANGED"); + + updated = mProvider.update(ReadingListItems.CONTENT_URI, updates, + ReadingListItems._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is(updated, 1, "Inserted item was updated"); + } finally { + c.close(); + } + + // Name change for clarity. These values will be compared with the + // current cursor row. + ContentValues expectedValues = updates; + c = getItemById(id); + try { + mAsserter.ok(c.moveToFirst(), "Updated item found", ""); + mAsserter.isnot(c.getLong(c.getColumnIndex(ReadingListItems.DATE_MODIFIED)), + originalDateModified, + "Date modified should have changed"); + + // DATE_CREATED and LENGTH should equal old values since they weren't updated. + expectedValues.put(ReadingListItems.DATE_CREATED, originalDateCreated); + expectedValues.put(ReadingListItems.LENGTH, original.getAsString(ReadingListItems.LENGTH)); + assertRowEqualsContentValues(c, expectedValues, /* compareDateModified */ false); + } finally { + c.close(); + } + + // Test that updates on an item that doesn't exist does not modify any rows. + testUpdateWithInvalidID(); + + // Test that update fails when a GUID is null. + testUpdateWithNullCol(id, ReadingListItems.GUID); + + mContentProviderUpdateTested = true; + } + + /** + * Test that updates on an item that doesn't exist does + * not modify any rows. + * + * @param id of the item to be deleted + */ + private void testUpdateWithInvalidID() { + ensureEmptyDatabase(); + final ContentValues b = createFillerReadingListItem(); + final long id = ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, b)); + final long INVALID_ID = id + 1; + final ContentValues updates = new ContentValues(); + updates.put(ReadingListItems.TITLE, b.getAsString(ReadingListItems.TITLE) + "CHANGED"); + final int updated = mProvider.update(ReadingListItems.CONTENT_URI, updates, + ReadingListItems._ID + " = ?", + new String[] { String.valueOf(INVALID_ID) }); + mAsserter.is(updated, 0, "Should not be able to update item with an invalid GUID"); + } + + /** + * Test that update fails when a required column is null. + */ + private int testUpdateWithNullCol(long id, String colName) { + ContentValues updates = new ContentValues(); + updates.putNull(colName); + + int updated = mProvider.update(ReadingListItems.CONTENT_URI, updates, + ReadingListItems._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is(updated, 0, "Should not be able to update item with " + colName + " == null "); + return updated; + } + } + + private class TestBatchOperations extends TestCase { + private static final int ITEM_COUNT = 10; + + /** + * Insert a bunch of items into the DB with the bulkInsert + * method and verify that they are there. + */ + private void testBulkInsert() { + ensureEmptyDatabase(); + final ContentValues allVals[] = new ContentValues[ITEM_COUNT]; + final HashSet urls = new HashSet(); + for (int i = 0; i < ITEM_COUNT; i++) { + final String url = "http://www.test.org/" + i; + allVals[i] = new ContentValues(); + allVals[i].put(ReadingListItems.TITLE, "Test" + i); + allVals[i].put(ReadingListItems.URL, url); + allVals[i].put(ReadingListItems.EXCERPT, "EXCERPT" + i); + allVals[i].put(ReadingListItems.LENGTH, i); + urls.add(url); + } + + int inserts = mProvider.bulkInsert(ReadingListItems.CONTENT_URI, allVals); + mAsserter.is(inserts, ITEM_COUNT, "Excepted number of inserts matches"); + + Cursor c = mProvider.query(ReadingListItems.CONTENT_URI, null, + null, + null, + null); + try { + while (c.moveToNext()) { + final String url = c.getString(c.getColumnIndex(ReadingListItems.URL)); + mAsserter.ok(urls.contains(url), "Bulk inserted item with url == " + url + " was found in the DB", ""); + // We should only be seeing each item once. Remove from set to prevent dups. + urls.remove(url); + } + } finally { + c.close(); + } + } + + @Override + public void test() { + testBulkInsert(); + } + } + + /* + * Verify that insert, update, delete, and bulkInsert operations + * notify the ambient content resolver. Each operation calls the + * content resolver notifyChange method synchronously, so it is + * okay to test sequentially. + */ + private class TestBrowserProviderNotifications extends TestCase { + + @Override + public void test() { + // We should be able to insert into the DB. + ensureCanInsert(); + // We should be able to update the DB. + ensureCanUpdate(); + + final String CONTENT_URI = ReadingListItems.CONTENT_URI.toString(); + + mResolver.notifyChangeList.clear(); + + // Insert + final ContentValues h = createFillerReadingListItem(); + long id = ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, h)); + + mAsserter.isnot(id, + -1L, + "Inserted item has valid id"); + + ensureOnlyChangeNotifiedStartsWith(CONTENT_URI, "insert"); + + // Update + mResolver.notifyChangeList.clear(); + h.put(ReadingListItems.TITLE, "http://newexample.com"); + + long numUpdated = mProvider.update(ReadingListItems.CONTENT_URI, h, + ReadingListItems._ID + " = ?", + new String[] { String.valueOf(id) }); + + mAsserter.is(numUpdated, + 1L, + "Correct number of items are updated"); + + ensureOnlyChangeNotifiedStartsWith(CONTENT_URI, "update"); + + // Delete + mResolver.notifyChangeList.clear(); + long numDeleted = mProvider.delete(ReadingListItems.CONTENT_URI, null, null); + + mAsserter.is(numDeleted, + 1L, + "Correct number of items are deleted"); + + ensureOnlyChangeNotifiedStartsWith(CONTENT_URI, "delete"); + + // Bulk insert + mResolver.notifyChangeList.clear(); + final ContentValues[] hs = { createFillerReadingListItem(), + createFillerReadingListItem(), + createFillerReadingListItem() }; + + long numBulkInserted = mProvider.bulkInsert(ReadingListItems.CONTENT_URI, hs); + + mAsserter.is(numBulkInserted, + 3L, + "Correct number of items are bulkInserted"); + + ensureOnlyChangeNotifiedStartsWith(CONTENT_URI, "bulkInsert"); + } + + protected void ensureOnlyChangeNotifiedStartsWith(String expectedUri, String operation) { + mAsserter.is(Long.valueOf(mResolver.notifyChangeList.size()), + 1L, + "Content observer was notified exactly once by " + operation); + + final Uri uri = mResolver.notifyChangeList.poll(); + + mAsserter.isnot(uri, + null, + "Notification from " + operation + " was valid"); + + mAsserter.ok(uri.toString().startsWith(expectedUri), + "Content observer was notified exactly once by " + operation, + ""); + } + } + + /** + * Removes all items from the DB. + */ + private void ensureEmptyDatabase() { + Uri uri = appendUriParam(ReadingListItems.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"); + getWritableDatabase(uri).delete(ReadingListItems.TABLE_NAME, null, null); + } + + + private SQLiteDatabase getWritableDatabase(Uri uri) { + Uri testUri = appendUriParam(uri, BrowserContract.PARAM_IS_TEST, "1"); + DelegatingTestContentProvider delegateProvider = (DelegatingTestContentProvider) mProvider; + ReadingListProvider readingListProvider = (ReadingListProvider) delegateProvider.getTargetProvider(); + return readingListProvider.getWritableDatabaseForTesting(testUri); + } + + /** + * Checks that the values in the cursor's current row match those + * in the ContentValues object. + * + * @param cursor over the row to be checked + * @param values to be checked + */ + private void assertRowEqualsContentValues(Cursor cursorWithActual, ContentValues expectedValues, boolean compareDateModified) { + for (String column: TEST_COLUMNS) { + String expected = expectedValues.getAsString(column); + String actual = cursorWithActual.getString(cursorWithActual.getColumnIndex(column)); + mAsserter.is(actual, expected, "Item has correct " + column); + } + + if (compareDateModified) { + String expected = expectedValues.getAsString(ReadingListItems.DATE_MODIFIED); + String actual = cursorWithActual.getString(cursorWithActual.getColumnIndex(ReadingListItems.DATE_MODIFIED)); + mAsserter.is(actual, expected, "Item has correct " + ReadingListItems.DATE_MODIFIED); + } + } + + private void assertRowEqualsContentValues(Cursor cursorWithActual, ContentValues expectedValues) { + assertRowEqualsContentValues(cursorWithActual, expectedValues, true); + } + + private ContentValues fillContentValues(String title, String url, String excerpt) { + ContentValues values = new ContentValues(); + + values.put(ReadingListItems.TITLE, title); + values.put(ReadingListItems.URL, url); + values.put(ReadingListItems.EXCERPT, excerpt); + values.put(ReadingListItems.LENGTH, excerpt.length()); + + return values; + } + + private ContentValues createFillerReadingListItem() { + Random rand = new Random(); + return fillContentValues("Example", "http://example.com/?num=" + rand.nextInt(), "foo bar"); + } + + private Cursor getItemById(Uri uri, long id, String[] projection) { + return mProvider.query(uri, projection, + ReadingListItems._ID + " = ?", + new String[] { String.valueOf(id) }, + null); + } + + private Cursor getItemById(long id) { + return getItemById(ReadingListItems.CONTENT_URI, id, null); + } + + private Cursor getItemById(Uri uri, long id) { + return getItemById(uri, id, null); + } + + /** + * Verifies that ContentProvider insertions have been tested. + */ + private void ensureCanInsert() { + if (!mContentProviderInsertTested) { + mAsserter.ok(false, "ContentProvider insertions have not been tested yet.", ""); + } + } + + /** + * Verifies that ContentProvider updates have been tested. + */ + private void ensureCanUpdate() { + if (!mContentProviderUpdateTested) { + mAsserter.ok(false, "ContentProvider updates have not been tested yet.", ""); + } + } + + private long insertAnItemWithAssertion() { + // We should be able to insert into the DB. + ensureCanInsert(); + + ContentValues v = createFillerReadingListItem(); + long id = ContentUris.parseId(mProvider.insert(ReadingListItems.CONTENT_URI, v)); + + assertItemExistsByID(id, "Inserted item found"); + return id; + } + + private void assertItemExistsByID(Uri uri, long id, String msg) { + Cursor c = getItemById(uri, id); + try { + mAsserter.ok(c.moveToFirst(), msg, ""); + } finally { + c.close(); + } + } + + private void assertItemExistsByID(long id, String msg) { + Cursor c = getItemById(id); + try { + mAsserter.ok(c.moveToFirst(), msg, ""); + } finally { + c.close(); + } + } + + private void assertItemDoesNotExistByID(long id, String msg) { + Cursor c = getItemById(id); + try { + mAsserter.ok(!c.moveToFirst(), msg, ""); + } finally { + c.close(); + } + } + + private void assertItemDoesNotExistByID(Uri uri, long id, String msg) { + Cursor c = getItemById(uri, id); + try { + mAsserter.ok(!c.moveToFirst(), msg, ""); + } finally { + c.close(); + } + } +}