/* 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(); } } }