/* 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.JSONObject; import org.mozilla.android.sync.test.helpers.ExpectFetchDelegate; import org.mozilla.android.sync.test.helpers.ExpectFinishDelegate; import org.mozilla.android.sync.test.helpers.HistoryHelpers; 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.Repository; import org.mozilla.gecko.sync.repositories.RepositorySession; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryDataAccessor; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepository; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepositorySession; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepository; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor; import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositorySession; 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.HistoryRecord; import org.mozilla.gecko.sync.repositories.domain.Record; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; public class AndroidBrowserHistoryRepositoryTest 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 AndroidBrowserHistoryRepository() { @Override protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) { AndroidBrowserHistoryRepositorySession session; session = new AndroidBrowserHistoryRepositorySession(this, context) { @Override protected synchronized void trackGUID(String guid) { System.out.println("Ignoring trackGUID call: this is a test!"); } }; delegate.onSessionCreated(session); } }; } @Override protected AndroidBrowserRepositoryDataAccessor getDataAccessor() { return new AndroidBrowserHistoryDataAccessor(getApplicationContext()); } @Override protected void closeDataAccessor(AndroidBrowserRepositoryDataAccessor dataAccessor) { if (!(dataAccessor instanceof AndroidBrowserHistoryDataAccessor)) { throw new IllegalArgumentException("Only expecting a history data accessor."); } ((AndroidBrowserHistoryDataAccessor) dataAccessor).closeExtender(); } @Override public void testFetchAll() { Record[] expected = new Record[2]; expected[0] = HistoryHelpers.createHistory3(); expected[1] = HistoryHelpers.createHistory2(); basicFetchAllTest(expected); } /* * 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(HistoryHelpers.createHistory1()); } @Override public void testCleanMultipleRecords() { cleanMultipleRecords( HistoryHelpers.createHistory1(), HistoryHelpers.createHistory2(), HistoryHelpers.createHistory3(), HistoryHelpers.createHistory4(), HistoryHelpers.createHistory5() ); } @Override public void testGuidsSinceReturnMultipleRecords() { HistoryRecord record0 = HistoryHelpers.createHistory1(); HistoryRecord record1 = HistoryHelpers.createHistory2(); guidsSinceReturnMultipleRecords(record0, record1); } @Override public void testGuidsSinceReturnNoRecords() { guidsSinceReturnNoRecords(HistoryHelpers.createHistory3()); } @Override public void testFetchSinceOneRecord() { fetchSinceOneRecord(HistoryHelpers.createHistory1(), HistoryHelpers.createHistory2()); } @Override public void testFetchSinceReturnNoRecords() { fetchSinceReturnNoRecords(HistoryHelpers.createHistory3()); } @Override public void testFetchOneRecordByGuid() { fetchOneRecordByGuid(HistoryHelpers.createHistory1(), HistoryHelpers.createHistory2()); } @Override public void testFetchMultipleRecordsByGuids() { HistoryRecord record0 = HistoryHelpers.createHistory1(); HistoryRecord record1 = HistoryHelpers.createHistory2(); HistoryRecord record2 = HistoryHelpers.createHistory3(); fetchMultipleRecordsByGuids(record0, record1, record2); } @Override public void testFetchNoRecordByGuid() { fetchNoRecordByGuid(HistoryHelpers.createHistory1()); } @Override public void testWipe() { doWipe(HistoryHelpers.createHistory2(), HistoryHelpers.createHistory3()); } @Override public void testStore() { basicStoreTest(HistoryHelpers.createHistory1()); } @Override public void testRemoteNewerTimeStamp() { HistoryRecord local = HistoryHelpers.createHistory1(); HistoryRecord remote = HistoryHelpers.createHistory2(); remoteNewerTimeStamp(local, remote); } @Override public void testLocalNewerTimeStamp() { HistoryRecord local = HistoryHelpers.createHistory1(); HistoryRecord remote = HistoryHelpers.createHistory2(); localNewerTimeStamp(local, remote); } @Override public void testDeleteRemoteNewer() { HistoryRecord local = HistoryHelpers.createHistory1(); HistoryRecord remote = HistoryHelpers.createHistory2(); deleteRemoteNewer(local, remote); } @Override public void testDeleteLocalNewer() { HistoryRecord local = HistoryHelpers.createHistory1(); HistoryRecord remote = HistoryHelpers.createHistory2(); deleteLocalNewer(local, remote); } @Override public void testDeleteRemoteLocalNonexistent() { deleteRemoteLocalNonexistent(HistoryHelpers.createHistory2()); } /** * Exists to provide access to record string logic. */ protected class HelperHistorySession extends AndroidBrowserHistoryRepositorySession { public HelperHistorySession(Repository repository, Context context) { super(repository, context); } public boolean sameRecordString(HistoryRecord r1, HistoryRecord r2) { return buildRecordString(r1).equals(buildRecordString(r2)); } } /** * Verifies that two history records with the same URI but different * titles will be reconciled locally. */ public void testRecordStringCollisionAndEquality() { final AndroidBrowserHistoryRepository repo = new AndroidBrowserHistoryRepository(); final HelperHistorySession testSession = new HelperHistorySession(repo, getApplicationContext()); final long now = RepositorySession.now(); final HistoryRecord record0 = new HistoryRecord(null, "history", now + 1, false); final HistoryRecord record1 = new HistoryRecord(null, "history", now + 2, false); final HistoryRecord record2 = new HistoryRecord(null, "history", now + 3, false); record0.histURI = "http://example.com/foo"; record1.histURI = "http://example.com/foo"; record2.histURI = "http://example.com/bar"; record0.title = "Foo 0"; record1.title = "Foo 1"; record2.title = "Foo 2"; // Ensure that two records with the same URI produce the same record string, // and two records with different URIs do not. assertTrue(testSession.sameRecordString(record0, record1)); assertFalse(testSession.sameRecordString(record0, record2)); // Two records are congruent if they have the same URI and their // identifiers match (which is why these all have null GUIDs). assertTrue(record0.congruentWith(record0)); assertTrue(record0.congruentWith(record1)); assertTrue(record1.congruentWith(record0)); assertFalse(record0.congruentWith(record2)); assertFalse(record1.congruentWith(record2)); assertFalse(record2.congruentWith(record1)); assertFalse(record2.congruentWith(record0)); // None of these records are equal, because they have different titles. // (Except for being equal to themselves, of course.) assertTrue(record0.equalPayloads(record0)); assertTrue(record1.equalPayloads(record1)); assertTrue(record2.equalPayloads(record2)); assertFalse(record0.equalPayloads(record1)); assertFalse(record1.equalPayloads(record0)); assertFalse(record1.equalPayloads(record2)); } /* * Tests for adding some visits to a history record * and doing a fetch. */ @SuppressWarnings("unchecked") public void testAddOneVisit() { final RepositorySession session = createAndBeginSession(); HistoryRecord record0 = HistoryHelpers.createHistory3(); performWait(storeRunnable(session, record0)); // Add one visit to the count and put in a new // last visited date. ContentValues cv = new ContentValues(); int visits = record0.visits.size() + 1; long newVisitTime = System.currentTimeMillis(); cv.put(BrowserContract.History.VISITS, visits); cv.put(BrowserContract.History.DATE_LAST_VISITED, newVisitTime); final AndroidBrowserRepositoryDataAccessor dataAccessor = getDataAccessor(); dataAccessor.updateByGuid(record0.guid, cv); // Add expected visit to record for verification. JSONObject expectedVisit = new JSONObject(); expectedVisit.put("date", newVisitTime * 1000); // Microseconds. expectedVisit.put("type", 1L); record0.visits.add(expectedVisit); performWait(fetchRunnable(session, new String[] { record0.guid }, new ExpectFetchDelegate(new Record[] { record0 }))); closeDataAccessor(dataAccessor); } @SuppressWarnings("unchecked") public void testAddMultipleVisits() { final RepositorySession session = createAndBeginSession(); HistoryRecord record0 = HistoryHelpers.createHistory4(); performWait(storeRunnable(session, record0)); // Add three visits to the count and put in a new // last visited date. ContentValues cv = new ContentValues(); int visits = record0.visits.size() + 3; long newVisitTime = System.currentTimeMillis(); cv.put(BrowserContract.History.VISITS, visits); cv.put(BrowserContract.History.DATE_LAST_VISITED, newVisitTime); final AndroidBrowserRepositoryDataAccessor dataAccessor = getDataAccessor(); dataAccessor.updateByGuid(record0.guid, cv); // Now shift to microsecond timing for visits. long newMicroVisitTime = newVisitTime * 1000; // Add expected visits to record for verification JSONObject expectedVisit = new JSONObject(); expectedVisit.put("date", newMicroVisitTime); expectedVisit.put("type", 1L); record0.visits.add(expectedVisit); expectedVisit = new JSONObject(); expectedVisit.put("date", newMicroVisitTime - 1000); expectedVisit.put("type", 1L); record0.visits.add(expectedVisit); expectedVisit = new JSONObject(); expectedVisit.put("date", newMicroVisitTime - 2000); expectedVisit.put("type", 1L); record0.visits.add(expectedVisit); ExpectFetchDelegate delegate = new ExpectFetchDelegate(new Record[] { record0 }); performWait(fetchRunnable(session, new String[] { record0.guid }, delegate)); Record fetched = delegate.records.get(0); assertTrue(record0.equalPayloads(fetched)); closeDataAccessor(dataAccessor); } public void testInvalidHistoryItemIsSkipped() throws NullCursorException { final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession(); final AndroidBrowserRepositoryDataAccessor dbHelper = session.getDBHelper(); final long now = System.currentTimeMillis(); final HistoryRecord emptyURL = new HistoryRecord(Utils.generateGuid(), "history", now, false); final HistoryRecord noVisits = new HistoryRecord(Utils.generateGuid(), "history", now, false); final HistoryRecord aboutURL = new HistoryRecord(Utils.generateGuid(), "history", now, false); emptyURL.fennecDateVisited = now; emptyURL.fennecVisitCount = 1; emptyURL.histURI = ""; emptyURL.title = "Something"; noVisits.fennecDateVisited = now; noVisits.fennecVisitCount = 0; noVisits.histURI = "http://example.org/novisits"; noVisits.title = "Something Else"; aboutURL.fennecDateVisited = now; aboutURL.fennecVisitCount = 1; aboutURL.histURI = "about:home"; aboutURL.title = "Fennec Home"; Uri one = dbHelper.insert(emptyURL); Uri two = dbHelper.insert(noVisits); Uri tre = dbHelper.insert(aboutURL); assertNotNull(one); assertNotNull(two); assertNotNull(tre); // The records are in the DB. final Cursor all = dbHelper.fetchAll(); assertEquals(3, all.getCount()); all.close(); // But aren't returned by fetching. performWait(fetchAllRunnable(session, new Record[] {})); // And we'd ignore about:home if we downloaded it. assertTrue(session.shouldIgnore(aboutURL)); session.abort(); } public void testSqlInjectPurgeDelete() { // Some setup. RepositorySession session = createAndBeginSession(); final AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); try { ContentValues cv = new ContentValues(); cv.put(BrowserContract.SyncColumns.IS_DELETED, 1); // Create and insert 2 history entries, 2nd one is evil (attempts injection). HistoryRecord h1 = HistoryHelpers.createHistory1(); HistoryRecord h2 = HistoryHelpers.createHistory2(); h2.guid = "' or '1'='1"; db.insert(h1); db.insert(h2); // Test 1 - updateByGuid() handles evil history entries correctly. db.updateByGuid(h2.guid, cv); // Query history table. Cursor cur = getAllHistory(); int numHistory = cur.getCount(); // Ensure only the evil history entry 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(h2.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 = getAllHistory(); int numHistoryAfterDeletion = cur.getCount(); // Ensure we have only 1 deleted row. assertEquals(numHistoryAfterDeletion, numHistory - 1); // Ensure only the evil history 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(h2.guid)) { fail("Evil guid was not deleted!"); } else { assertFalse(deleted); } cur.moveToNext(); } } finally { cur.close(); } } finally { closeDataAccessor(db); session.abort(); } } protected Cursor getAllHistory() { Context context = getApplicationContext(); Cursor cur = context.getContentResolver().query(BrowserContractHelpers.HISTORY_CONTENT_URI, BrowserContractHelpers.HistoryColumns, null, null, null); return cur; } public void testDataAccessorBulkInsert() throws NullCursorException { final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession(); AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper(); ArrayList<HistoryRecord> records = new ArrayList<HistoryRecord>(); records.add(HistoryHelpers.createHistory1()); records.add(HistoryHelpers.createHistory2()); records.add(HistoryHelpers.createHistory3()); db.bulkInsert(records); performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(records.toArray(new Record[records.size()])))); session.abort(); } public void testDataExtenderIsClosedBeforeBegin() { // Create a session but don't begin() it. final AndroidBrowserRepositorySession session = (AndroidBrowserRepositorySession) createSession(); AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper(); // Confirm dataExtender is closed before beginning session. assertTrue(db.getHistoryDataExtender().isClosed()); } public void testDataExtenderIsClosedAfterFinish() throws InactiveSessionException { final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession(); AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper(); // Perform an action that opens the dataExtender. HistoryRecord h1 = HistoryHelpers.createHistory1(); db.insert(h1); assertFalse(db.getHistoryDataExtender().isClosed()); // Check dataExtender is closed upon finish. performWait(finishRunnable(session, new ExpectFinishDelegate())); assertTrue(db.getHistoryDataExtender().isClosed()); } public void testDataExtenderIsClosedAfterAbort() throws InactiveSessionException { final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession(); AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper(); // Perform an action that opens the dataExtender. HistoryRecord h1 = HistoryHelpers.createHistory1(); db.insert(h1); assertFalse(db.getHistoryDataExtender().isClosed()); // Check dataExtender is closed upon abort. session.abort(); assertTrue(db.getHistoryDataExtender().isClosed()); } }