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