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.HashSet; michael@0: import java.util.Set; michael@0: michael@0: import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; michael@0: import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate; michael@0: import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate; michael@0: import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate; michael@0: import org.mozilla.gecko.background.sync.helpers.ExpectStoredDelegate; michael@0: import org.mozilla.gecko.background.sync.helpers.PasswordHelpers; michael@0: import org.mozilla.gecko.background.sync.helpers.SessionTestHelper; michael@0: import org.mozilla.gecko.background.testhelpers.WaitHelper; 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.NoStoreDelegateException; 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.BrowserContractHelpers; michael@0: import org.mozilla.gecko.sync.repositories.android.PasswordsRepositorySession; 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.PasswordRecord; michael@0: import org.mozilla.gecko.sync.repositories.domain.Record; michael@0: michael@0: import android.content.ContentProviderClient; michael@0: import android.content.Context; michael@0: import android.database.Cursor; michael@0: import android.os.RemoteException; michael@0: michael@0: public class TestPasswordsRepository extends AndroidSyncTestCase { michael@0: private final String NEW_PASSWORD1 = "password"; michael@0: private final String NEW_PASSWORD2 = "drowssap"; michael@0: michael@0: @Override michael@0: public void setUp() { michael@0: wipe(); michael@0: assertTrue(WaitHelper.getTestWaiter().isIdle()); michael@0: } michael@0: michael@0: public void testFetchAll() { michael@0: RepositorySession session = createAndBeginSession(); michael@0: Record[] expected = new Record[] { PasswordHelpers.createPassword1(), michael@0: PasswordHelpers.createPassword2() }; michael@0: michael@0: performWait(storeRunnable(session, expected[0])); michael@0: performWait(storeRunnable(session, expected[1])); michael@0: michael@0: performWait(fetchAllRunnable(session, expected)); michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testGuidsSinceReturnMultipleRecords() { michael@0: RepositorySession session = createAndBeginSession(); michael@0: michael@0: PasswordRecord record1 = PasswordHelpers.createPassword1(); michael@0: PasswordRecord record2 = PasswordHelpers.createPassword2(); michael@0: michael@0: updatePassword(NEW_PASSWORD1, record1); michael@0: long timestamp = updatePassword(NEW_PASSWORD2, record2); michael@0: michael@0: String[] expected = new String[] { record1.guid, record2.guid }; michael@0: michael@0: performWait(storeRunnable(session, record1)); michael@0: performWait(storeRunnable(session, record2)); michael@0: michael@0: performWait(guidsSinceRunnable(session, timestamp, expected)); michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testGuidsSinceReturnNoRecords() { michael@0: RepositorySession session = createAndBeginSession(); michael@0: michael@0: // Store 1 record in the past. michael@0: performWait(storeRunnable(session, PasswordHelpers.createPassword1())); michael@0: michael@0: String[] expected = {}; michael@0: performWait(guidsSinceRunnable(session, System.currentTimeMillis() + 1000, expected)); michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testFetchSinceOneRecord() { michael@0: RepositorySession session = createAndBeginSession(); michael@0: michael@0: // Passwords fetchSince checks timePasswordChanged, not insertion time. michael@0: PasswordRecord record1 = PasswordHelpers.createPassword1(); michael@0: long timeModified1 = updatePassword(NEW_PASSWORD1, record1); michael@0: performWait(storeRunnable(session, record1)); michael@0: michael@0: PasswordRecord record2 = PasswordHelpers.createPassword2(); michael@0: long timeModified2 = updatePassword(NEW_PASSWORD2, record2); michael@0: performWait(storeRunnable(session, record2)); michael@0: michael@0: String[] expectedOne = new String[] { record2.guid }; michael@0: performWait(fetchSinceRunnable(session, timeModified2 - 10, expectedOne)); michael@0: michael@0: String[] expectedBoth = new String[] { record1.guid, record2.guid }; michael@0: performWait(fetchSinceRunnable(session, timeModified1 - 10, expectedBoth)); michael@0: michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testFetchSinceReturnNoRecords() { michael@0: RepositorySession session = createAndBeginSession(); michael@0: michael@0: performWait(storeRunnable(session, PasswordHelpers.createPassword2())); michael@0: michael@0: long timestamp = System.currentTimeMillis(); michael@0: michael@0: performWait(fetchSinceRunnable(session, timestamp + 2000, new String[] {})); michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testFetchOneRecordByGuid() { michael@0: RepositorySession session = createAndBeginSession(); michael@0: Record record = PasswordHelpers.createPassword1(); michael@0: performWait(storeRunnable(session, record)); michael@0: performWait(storeRunnable(session, PasswordHelpers.createPassword2())); michael@0: michael@0: String[] guids = new String[] { record.guid }; michael@0: Record[] expected = new Record[] { record }; michael@0: performWait(fetchRunnable(session, guids, expected)); michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testFetchMultipleRecordsByGuids() { michael@0: RepositorySession session = createAndBeginSession(); michael@0: PasswordRecord record1 = PasswordHelpers.createPassword1(); michael@0: PasswordRecord record2 = PasswordHelpers.createPassword2(); michael@0: PasswordRecord record3 = PasswordHelpers.createPassword3(); michael@0: michael@0: performWait(storeRunnable(session, record1)); michael@0: performWait(storeRunnable(session, record2)); michael@0: performWait(storeRunnable(session, record3)); michael@0: michael@0: String[] guids = new String[] { record1.guid, record2.guid }; michael@0: Record[] expected = new Record[] { record1, record2 }; michael@0: performWait(fetchRunnable(session, guids, expected)); michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testFetchNoRecordByGuid() { michael@0: RepositorySession session = createAndBeginSession(); michael@0: Record record = PasswordHelpers.createPassword1(); michael@0: michael@0: performWait(storeRunnable(session, record)); michael@0: performWait(fetchRunnable(session, michael@0: new String[] { Utils.generateGuid() }, michael@0: new Record[] {})); michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testStore() { michael@0: final RepositorySession session = createAndBeginSession(); michael@0: performWait(storeRunnable(session, PasswordHelpers.createPassword1())); michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testRemoteNewerTimeStamp() { michael@0: final RepositorySession session = createAndBeginSession(); michael@0: michael@0: // Store updated local record. michael@0: PasswordRecord local = PasswordHelpers.createPassword1(); michael@0: updatePassword(NEW_PASSWORD1, local, System.currentTimeMillis() - 1000); michael@0: performWait(storeRunnable(session, local)); michael@0: michael@0: // Sync a remote record version that is newer. michael@0: PasswordRecord remote = PasswordHelpers.createPassword2(); michael@0: remote.guid = local.guid; michael@0: updatePassword(NEW_PASSWORD2, remote); michael@0: performWait(storeRunnable(session, remote)); michael@0: michael@0: // Make a fetch, expecting only the newer (remote) record. michael@0: performWait(fetchAllRunnable(session, new Record[] { remote })); michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testLocalNewerTimeStamp() { michael@0: final RepositorySession session = createAndBeginSession(); michael@0: // Remote record updated before local record. michael@0: PasswordRecord remote = PasswordHelpers.createPassword1(); michael@0: updatePassword(NEW_PASSWORD1, remote, System.currentTimeMillis() - 1000); michael@0: michael@0: // Store updated local record. michael@0: PasswordRecord local = PasswordHelpers.createPassword2(); michael@0: updatePassword(NEW_PASSWORD2, local); michael@0: performWait(storeRunnable(session, local)); michael@0: michael@0: // Sync a remote record version that is older. michael@0: remote.guid = local.guid; michael@0: performWait(storeRunnable(session, remote)); michael@0: michael@0: // Make a fetch, expecting only the newer (local) record. michael@0: performWait(fetchAllRunnable(session, new Record[] { local })); michael@0: dispose(session); michael@0: } michael@0: michael@0: /* michael@0: * Store two records that are identical except for guid. Expect to find the michael@0: * remote one after reconciling. michael@0: */ michael@0: public void testStoreIdenticalExceptGuid() { michael@0: RepositorySession session = createAndBeginSession(); michael@0: PasswordRecord record = PasswordHelpers.createPassword1(); michael@0: record.guid = "before1"; michael@0: // Store record. michael@0: performWait(storeRunnable(session, record)); michael@0: michael@0: // Store same record, but with different guid. michael@0: record.guid = Utils.generateGuid(); michael@0: performWait(storeRunnable(session, record)); michael@0: michael@0: performWait(fetchAllRunnable(session, new Record[] { record })); michael@0: dispose(session); michael@0: michael@0: session = createAndBeginSession(); michael@0: michael@0: PasswordRecord record2 = PasswordHelpers.createPassword2(); michael@0: record2.guid = "before2"; michael@0: // Store record. michael@0: performWait(storeRunnable(session, record2)); michael@0: michael@0: // Store same record, but with different guid. michael@0: record2.guid = Utils.generateGuid(); michael@0: performWait(storeRunnable(session, record2)); michael@0: michael@0: performWait(fetchAllRunnable(session, new Record[] { record, record2 })); michael@0: dispose(session); michael@0: } michael@0: michael@0: /* michael@0: * Store two records that are identical except for guid when they both point michael@0: * to the same site and there are multiple records for that site. Expect to michael@0: * find the remote one after reconciling. michael@0: */ michael@0: public void testStoreIdenticalExceptGuidOnSameSite() { michael@0: RepositorySession session = createAndBeginSession(); michael@0: PasswordRecord record1 = PasswordHelpers.createPassword1(); michael@0: record1.encryptedUsername = "original"; michael@0: record1.guid = "before1"; michael@0: PasswordRecord record2 = PasswordHelpers.createPassword1(); michael@0: record2.encryptedUsername = "different"; michael@0: record1.guid = "before2"; michael@0: // Store records. michael@0: performWait(storeRunnable(session, record1)); michael@0: performWait(storeRunnable(session, record2)); michael@0: performWait(fetchAllRunnable(session, new Record[] { record1, record2 })); michael@0: michael@0: dispose(session); michael@0: session = createAndBeginSession(); michael@0: michael@0: // Store same records, but with different guids. michael@0: record1.guid = Utils.generateGuid(); michael@0: performWait(storeRunnable(session, record1)); michael@0: performWait(fetchAllRunnable(session, new Record[] { record1, record2 })); michael@0: michael@0: record2.guid = Utils.generateGuid(); michael@0: performWait(storeRunnable(session, record2)); michael@0: performWait(fetchAllRunnable(session, new Record[] { record1, record2 })); michael@0: michael@0: dispose(session); michael@0: } michael@0: michael@0: public void testRawFetch() throws RemoteException { michael@0: RepositorySession session = createAndBeginSession(); michael@0: Record[] expected = new Record[] { PasswordHelpers.createPassword1(), michael@0: PasswordHelpers.createPassword2() }; michael@0: michael@0: performWait(storeRunnable(session, expected[0])); michael@0: performWait(storeRunnable(session, expected[1])); michael@0: michael@0: ContentProviderClient client = getApplicationContext().getContentResolver().acquireContentProviderClient(BrowserContract.PASSWORDS_AUTHORITY_URI); michael@0: Cursor cursor = client.query(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null, null, null); michael@0: assertEquals(2, cursor.getCount()); michael@0: cursor.moveToFirst(); michael@0: Set guids = new HashSet(); michael@0: while (!cursor.isAfterLast()) { michael@0: String guid = RepoUtils.getStringFromCursor(cursor, BrowserContract.Passwords.GUID); michael@0: guids.add(guid); michael@0: cursor.moveToNext(); michael@0: } michael@0: cursor.close(); michael@0: assertEquals(2, guids.size()); michael@0: assertTrue(guids.contains(expected[0].guid)); michael@0: assertTrue(guids.contains(expected[1].guid)); michael@0: dispose(session); michael@0: } michael@0: michael@0: // Helper methods. michael@0: private RepositorySession createAndBeginSession() { michael@0: return SessionTestHelper.createAndBeginSession( michael@0: getApplicationContext(), michael@0: getRepository()); michael@0: } michael@0: michael@0: private Repository getRepository() { michael@0: /** michael@0: * Override this chain in order to avoid our test code having to create two michael@0: * sessions all the time. Don't track records, so they filtering doesn't happen. michael@0: */ michael@0: return new PasswordsRepositorySession.PasswordsRepository() { michael@0: @Override michael@0: public void createSession(RepositorySessionCreationDelegate delegate, michael@0: Context context) { michael@0: PasswordsRepositorySession session; michael@0: session = new PasswordsRepositorySession(this, context) { michael@0: @Override michael@0: protected synchronized void trackGUID(String guid) { michael@0: } michael@0: }; michael@0: delegate.onSessionCreated(session); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: private void wipe() { michael@0: Context context = getApplicationContext(); michael@0: context.getContentResolver().delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null); michael@0: context.getContentResolver().delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, null, null); michael@0: } michael@0: michael@0: private static void dispose(RepositorySession session) { michael@0: if (session != null) { michael@0: session.abort(); michael@0: } michael@0: } michael@0: michael@0: private static long updatePassword(String password, PasswordRecord record, long timestamp) { michael@0: record.encryptedPassword = password; michael@0: long modifiedTime = System.currentTimeMillis(); michael@0: record.timePasswordChanged = record.lastModified = modifiedTime; michael@0: return modifiedTime; michael@0: } michael@0: michael@0: private static long updatePassword(String password, PasswordRecord record) { michael@0: return updatePassword(password, record, System.currentTimeMillis()); michael@0: } michael@0: michael@0: // Runnable Helpers. michael@0: private static Runnable storeRunnable(final RepositorySession session, final Record record) { michael@0: return new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: session.setStoreDelegate(new ExpectStoredDelegate(record.guid)); michael@0: try { michael@0: session.store(record); michael@0: session.storeDone(); michael@0: } catch (NoStoreDelegateException e) { michael@0: fail("NoStoreDelegateException should not occur."); michael@0: } michael@0: } michael@0: }; michael@0: } michael@0: michael@0: private static Runnable fetchAllRunnable(final RepositorySession session, final Record[] records) { michael@0: return new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: session.fetchAll(new ExpectFetchDelegate(records)); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: private static Runnable guidsSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) { michael@0: return new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: session.guidsSince(timestamp, new ExpectGuidsSinceDelegate(expected)); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: private static Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) { michael@0: return new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: session.fetchSince(timestamp, new ExpectFetchSinceDelegate(timestamp, expected)); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: private static Runnable fetchRunnable(final RepositorySession session, final String[] guids, final Record[] expected) { michael@0: return new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: try { michael@0: session.fetch(guids, new ExpectFetchDelegate(expected)); michael@0: } catch (InactiveSessionException e) { michael@0: performNotify(e); michael@0: } michael@0: } michael@0: }; michael@0: } michael@0: }