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