diff options
Diffstat (limited to 'mobile/android/tests/background/junit3')
80 files changed, 9507 insertions, 0 deletions
diff --git a/mobile/android/tests/background/junit3/AndroidManifest.xml.in b/mobile/android/tests/background/junit3/AndroidManifest.xml.in new file mode 100644 index 000000000..09f1a25a1 --- /dev/null +++ b/mobile/android/tests/background/junit3/AndroidManifest.xml.in @@ -0,0 +1,23 @@ +#filter substitution +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.mozilla.gecko.background.tests" + sharedUserId="@MOZ_ANDROID_SHARED_ID@" + android:versionCode="1" + android:versionName="1.0" > + + <uses-sdk android:minSdkVersion="8" + android:targetSdkVersion="@ANDROID_TARGET_SDK@" /> + + <application + android:debuggable="true" + android:icon="@drawable/icon" + android:label="@ANDROID_BACKGROUND_APP_DISPLAYNAME@"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:label="@string/app_name" + android:name="android.test.InstrumentationTestRunner" + android:targetPackage="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@" /> +</manifest> diff --git a/mobile/android/tests/background/junit3/Makefile.in b/mobile/android/tests/background/junit3/Makefile.in new file mode 100644 index 000000000..f7f40ca78 --- /dev/null +++ b/mobile/android/tests/background/junit3/Makefile.in @@ -0,0 +1,13 @@ +# 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/. + +ANDROID_EXTRA_JARS := \ + background-junit3.jar \ + $(NULL) + +ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml + +include $(topsrcdir)/config/rules.mk + +tools:: $(ANDROID_APK_NAME).apk diff --git a/mobile/android/tests/background/junit3/background_junit3_sources.mozbuild b/mobile/android/tests/background/junit3/background_junit3_sources.mozbuild new file mode 100644 index 000000000..021af2eb8 --- /dev/null +++ b/mobile/android/tests/background/junit3/background_junit3_sources.mozbuild @@ -0,0 +1,78 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +background_junit3_sources = [ + 'src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java', + 'src/org/mozilla/gecko/background/common/TestUtils.java', + 'src/org/mozilla/gecko/background/common/TestWaitHelper.java', + 'src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java', + 'src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java', + 'src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java', + 'src/org/mozilla/gecko/background/db/TestBookmarks.java', + 'src/org/mozilla/gecko/background/db/TestClientsDatabase.java', + 'src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java', + 'src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java', + 'src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java', + 'src/org/mozilla/gecko/background/db/TestPasswordsRepository.java', + 'src/org/mozilla/gecko/background/db/TestTopSites.java', + 'src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java', + 'src/org/mozilla/gecko/background/fxa/TestAccountLoader.java', + 'src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java', + 'src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java', + 'src/org/mozilla/gecko/background/helpers/DBHelpers.java', + 'src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java', + 'src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java', + 'src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java', + 'src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java', + 'src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java', + 'src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java', + 'src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java', + 'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java', + 'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java', + 'src/org/mozilla/gecko/background/sync/TestClientsStage.java', + 'src/org/mozilla/gecko/background/sync/TestResetting.java', + 'src/org/mozilla/gecko/background/sync/TestStoreTracking.java', + 'src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java', + 'src/org/mozilla/gecko/background/sync/TestWebURLFinder.java', + 'src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java', + 'src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java', + 'src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java', + 'src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java', + 'src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java', + 'src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java', + 'src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java', + 'src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java', + 'src/org/mozilla/gecko/background/testhelpers/MockRecord.java', + 'src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java', + 'src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java', + 'src/org/mozilla/gecko/background/testhelpers/WaitHelper.java', + 'src/org/mozilla/gecko/background/testhelpers/WBORepository.java', +] diff --git a/mobile/android/tests/background/junit3/instrumentation.ini b/mobile/android/tests/background/junit3/instrumentation.ini new file mode 100644 index 000000000..e2c7b2ea1 --- /dev/null +++ b/mobile/android/tests/background/junit3/instrumentation.ini @@ -0,0 +1,22 @@ +[DEFAULT] +subsuite = background + +[src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java] +[src/org/mozilla/gecko/background/common/TestUtils.java] +[src/org/mozilla/gecko/background/common/TestWaitHelper.java] +[src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java] +[src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java] +[src/org/mozilla/gecko/background/db/TestBookmarks.java] +[src/org/mozilla/gecko/background/db/TestClientsDatabase.java] +[src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java] +[src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java] +[src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java] +[src/org/mozilla/gecko/background/db/TestPasswordsRepository.java] +[src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java] +[src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java] +[src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java] +[src/org/mozilla/gecko/background/sync/TestClientsStage.java] +[src/org/mozilla/gecko/background/sync/TestResetting.java] +[src/org/mozilla/gecko/background/sync/TestStoreTracking.java] +[src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java] +[src/org/mozilla/gecko/background/sync/TestWebURLFinder.java] diff --git a/mobile/android/tests/background/junit3/moz.build b/mobile/android/tests/background/junit3/moz.build new file mode 100644 index 000000000..3e81e1e32 --- /dev/null +++ b/mobile/android/tests/background/junit3/moz.build @@ -0,0 +1,42 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME'] + +ANDROID_APK_NAME = 'background-junit3-debug' +ANDROID_APK_PACKAGE = 'org.mozilla.gecko.background.tests' + +include('background_junit3_sources.mozbuild') + +jar = add_java_jar('background-junit3') +jar.sources += background_junit3_sources +jar.extra_jars += [ + CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'], + CONFIG['ANDROID_RECYCLERVIEW_V7_AAR_LIB'], + TOPOBJDIR + '/mobile/android/base/constants.jar', + TOPOBJDIR + '/mobile/android/base/gecko-R.jar', + TOPOBJDIR + '/mobile/android/base/gecko-browser.jar', + TOPOBJDIR + '/mobile/android/base/gecko-mozglue.jar', + TOPOBJDIR + '/mobile/android/base/gecko-thirdparty.jar', + TOPOBJDIR + '/mobile/android/base/gecko-util.jar', + TOPOBJDIR + '/mobile/android/base/gecko-view.jar', + TOPOBJDIR + '/mobile/android/base/services.jar', + TOPOBJDIR + '/mobile/android/base/sync-thirdparty.jar', +] + +if CONFIG['MOZ_ANDROID_MLS_STUMBLER']: + jar.extra_jars += [ + TOPOBJDIR + '/mobile/android/stumbler/stumbler.jar', + ] + +ANDROID_INSTRUMENTATION_MANIFESTS += ['instrumentation.ini'] + +DEFINES['ANDROID_BACKGROUND_TARGET_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME'] +DEFINES['ANDROID_BACKGROUND_APP_DISPLAYNAME'] = '%s Background Tests' % CONFIG['MOZ_APP_DISPLAYNAME'] +DEFINES['MOZ_ANDROID_SHARED_ID'] = CONFIG['MOZ_ANDROID_SHARED_ID'] +OBJDIR_PP_FILES.mobile.android.tests.background.junit3 += [ + 'AndroidManifest.xml.in', +] diff --git a/mobile/android/tests/background/junit3/res/drawable-hdpi/icon.png b/mobile/android/tests/background/junit3/res/drawable-hdpi/icon.png Binary files differnew file mode 100644 index 000000000..e83438eee --- /dev/null +++ b/mobile/android/tests/background/junit3/res/drawable-hdpi/icon.png diff --git a/mobile/android/tests/background/junit3/res/drawable-ldpi/icon.png b/mobile/android/tests/background/junit3/res/drawable-ldpi/icon.png Binary files differnew file mode 100644 index 000000000..0483c95e9 --- /dev/null +++ b/mobile/android/tests/background/junit3/res/drawable-ldpi/icon.png diff --git a/mobile/android/tests/background/junit3/res/drawable-mdpi/icon.png b/mobile/android/tests/background/junit3/res/drawable-mdpi/icon.png Binary files differnew file mode 100644 index 000000000..86b4dee54 --- /dev/null +++ b/mobile/android/tests/background/junit3/res/drawable-mdpi/icon.png diff --git a/mobile/android/tests/background/junit3/res/layout/main.xml b/mobile/android/tests/background/junit3/res/layout/main.xml new file mode 100644 index 000000000..14dbff7e0 --- /dev/null +++ b/mobile/android/tests/background/junit3/res/layout/main.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" > + + <TextView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:text="@string/app_name" /> + +</LinearLayout> diff --git a/mobile/android/tests/background/junit3/res/values/strings.xml b/mobile/android/tests/background/junit3/res/values/strings.xml new file mode 100644 index 000000000..d6534d7fa --- /dev/null +++ b/mobile/android/tests/background/junit3/res/values/strings.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Gecko Background Tests</string> + +</resources> diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java new file mode 100644 index 000000000..7f4b9bb9c --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.common; + +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.common.log.writers.AndroidLevelCachingLogWriter; +import org.mozilla.gecko.background.common.log.writers.AndroidLogWriter; +import org.mozilla.gecko.background.common.log.writers.LogWriter; +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; + +public class TestAndroidLogWriters extends AndroidSyncTestCase { + public static final String TEST_LOG_TAG = "TestAndroidLogWriters"; + + public static final String TEST_MESSAGE_1 = "LOG TEST MESSAGE one"; + public static final String TEST_MESSAGE_2 = "LOG TEST MESSAGE two"; + public static final String TEST_MESSAGE_3 = "LOG TEST MESSAGE three"; + + public void setUp() { + Logger.stopLoggingToAll(); + } + + public void tearDown() { + Logger.resetLogging(); + } + + /** + * Verify these *all* appear in the Android log by using + * <code>adb logcat | grep TestAndroidLogWriters</code> after executing + * <code>adb shell setprop log.tag.TestAndroidLogWriters ERROR</code>. + * <p> + * This writer does not use the Android log levels! + */ + public void testAndroidLogWriter() { + LogWriter lw = new AndroidLogWriter(); + + Logger.error(TEST_LOG_TAG, TEST_MESSAGE_1, new RuntimeException()); + Logger.startLoggingTo(lw); + Logger.error(TEST_LOG_TAG, TEST_MESSAGE_2); + Logger.warn(TEST_LOG_TAG, TEST_MESSAGE_2); + Logger.info(TEST_LOG_TAG, TEST_MESSAGE_2); + Logger.debug(TEST_LOG_TAG, TEST_MESSAGE_2); + Logger.trace(TEST_LOG_TAG, TEST_MESSAGE_2); + Logger.stopLoggingTo(lw); + Logger.error(TEST_LOG_TAG, TEST_MESSAGE_3, new RuntimeException()); + } + + /** + * Verify only *some* of these appear in the Android log by using + * <code>adb logcat | grep TestAndroidLogWriters</code> after executing + * <code>adb shell setprop log.tag.TestAndroidLogWriters INFO</code>. + * <p> + * This writer should use the Android log levels! + */ + public void testAndroidLevelCachingLogWriter() throws Exception { + LogWriter lw = new AndroidLevelCachingLogWriter(new AndroidLogWriter()); + + Logger.error(TEST_LOG_TAG, TEST_MESSAGE_1, new RuntimeException()); + Logger.startLoggingTo(lw); + Logger.error(TEST_LOG_TAG, TEST_MESSAGE_2); + Logger.warn(TEST_LOG_TAG, TEST_MESSAGE_2); + Logger.info(TEST_LOG_TAG, TEST_MESSAGE_2); + Logger.debug(TEST_LOG_TAG, TEST_MESSAGE_2); + Logger.trace(TEST_LOG_TAG, TEST_MESSAGE_2); + Logger.stopLoggingTo(lw); + Logger.error(TEST_LOG_TAG, TEST_MESSAGE_3, new RuntimeException()); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestUtils.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestUtils.java new file mode 100644 index 000000000..270eae6f6 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestUtils.java @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.common; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.sync.Utils; + +import android.os.Bundle; + +public class TestUtils extends AndroidSyncTestCase { + protected static void assertStages(String[] all, String[] sync, String[] skip, String[] expected) { + final Set<String> sAll = new HashSet<String>(); + for (String s : all) { + sAll.add(s); + } + List<String> sSync = null; + if (sync != null) { + sSync = new ArrayList<String>(); + for (String s : sync) { + sSync.add(s); + } + } + List<String> sSkip = null; + if (skip != null) { + sSkip = new ArrayList<String>(); + for (String s : skip) { + sSkip.add(s); + } + } + List<String> stages = new ArrayList<String>(Utils.getStagesToSync(sAll, sSync, sSkip)); + Collections.sort(stages); + List<String> exp = new ArrayList<String>(); + for (String e : expected) { + exp.add(e); + } + assertEquals(exp, stages); + } + + public void testGetStagesToSync() { + final String[] all = new String[] { "other1", "other2", "skip1", "skip2", "sync1", "sync2" }; + assertStages(all, null, null, all); + assertStages(all, new String[] { "sync1" }, null, new String[] { "sync1" }); + assertStages(all, null, new String[] { "skip1", "skip2" }, new String[] { "other1", "other2", "sync1", "sync2" }); + assertStages(all, new String[] { "sync1", "sync2" }, new String[] { "skip1", "skip2" }, new String[] { "sync1", "sync2" }); + } + + protected static void assertStagesFromBundle(String[] all, String[] sync, String[] skip, String[] expected) { + final Set<String> sAll = new HashSet<String>(); + for (String s : all) { + sAll.add(s); + } + final Bundle bundle = new Bundle(); + Utils.putStageNamesToSync(bundle, sync, skip); + + Collection<String> ss = Utils.getStagesToSyncFromBundle(sAll, bundle); + List<String> stages = new ArrayList<String>(ss); + Collections.sort(stages); + List<String> exp = new ArrayList<String>(); + for (String e : expected) { + exp.add(e); + } + assertEquals(exp, stages); + } + + public void testGetStagesToSyncFromBundle() { + final String[] all = new String[] { "other1", "other2", "skip1", "skip2", "sync1", "sync2" }; + assertStagesFromBundle(all, null, null, all); + assertStagesFromBundle(all, new String[] { "sync1" }, null, new String[] { "sync1" }); + assertStagesFromBundle(all, null, new String[] { "skip1", "skip2" }, new String[] { "other1", "other2", "sync1", "sync2" }); + assertStagesFromBundle(all, new String[] { "sync1", "sync2" }, new String[] { "skip1", "skip2" }, new String[] { "sync1", "sync2" }); + } + + public static void deleteDirectoryRecursively(final File dir) throws IOException { + if (!dir.isDirectory()) { + throw new IllegalStateException("Given directory, " + dir + ", is not a directory!"); + } + + for (File f : dir.listFiles()) { + if (f.isDirectory()) { + deleteDirectoryRecursively(f); + } else if (!f.delete()) { + // Since this method is for testing, we assume we should be able to do this. + throw new IOException("Could not delete file, " + f.getAbsolutePath() + ". Permissions?"); + } + } + + if (!dir.delete()) { + throw new IOException("Could not delete dir, " + dir.getAbsolutePath() + "."); + } + } + + public void testDeleteDirectoryRecursively() throws Exception { + final String TEST_DIR = getApplicationContext().getCacheDir().getAbsolutePath() + + "-testDeleteDirectory-" + System.currentTimeMillis(); + + // Non-existent directory. + final File nonexistent = new File("nonexistentDirectory"); // Hopefully. ;) + assertFalse(nonexistent.exists()); + try { + deleteDirectoryRecursively(nonexistent); + fail("deleteDirectoryRecursively on a nonexistent directory should throw Exception"); + } catch (IllegalStateException e) { } + + // Empty dir. + File dir = mkdir(TEST_DIR); + deleteDirectoryRecursively(dir); + assertFalse(dir.exists()); + + // Filled dir. + dir = mkdir(TEST_DIR); + populateDir(dir); + deleteDirectoryRecursively(dir); + assertFalse(dir.exists()); + + // Filled dir with empty dir. + dir = mkdir(TEST_DIR); + populateDir(dir); + File subDir = new File(TEST_DIR + File.separator + "subDir"); + assertTrue(subDir.mkdir()); + deleteDirectoryRecursively(dir); + assertFalse(subDir.exists()); // For short-circuiting errors. + assertFalse(dir.exists()); + + // Filled dir with filled dir. + dir = mkdir(TEST_DIR); + populateDir(dir); + subDir = new File(TEST_DIR + File.separator + "subDir"); + assertTrue(subDir.mkdir()); + populateDir(subDir); + deleteDirectoryRecursively(dir); + assertFalse(subDir.exists()); // For short-circuiting errors. + assertFalse(dir.exists()); + } + + private File mkdir(final String name) { + final File dir = new File(name); + assertTrue(dir.mkdir()); + return dir; + } + + private void populateDir(final File dir) throws IOException { + assertTrue(dir.isDirectory()); + final String dirPath = dir.getAbsolutePath(); + for (int i = 0; i < 3; i++) { + final File f = new File(dirPath + File.separator + i); + assertTrue(f.createNewFile()); // Throws IOException if file could not be created. + } + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestWaitHelper.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestWaitHelper.java new file mode 100644 index 000000000..1f818e0cf --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestWaitHelper.java @@ -0,0 +1,356 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.common; + +import junit.framework.AssertionFailedError; + +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.background.testhelpers.WaitHelper.InnerError; +import org.mozilla.gecko.background.testhelpers.WaitHelper.TimeoutError; +import org.mozilla.gecko.sync.ThreadPool; + +public class TestWaitHelper extends AndroidSyncTestCase { + private static final String ERROR_UNIQUE_IDENTIFIER = "error unique identifier"; + + public static int NO_WAIT = 1; // Milliseconds. + public static int SHORT_WAIT = 100; // Milliseconds. + public static int LONG_WAIT = 3 * SHORT_WAIT; + + private Object notifyMonitor = new Object(); + // Guarded by notifyMonitor. + private boolean performNotifyCalled = false; + private boolean performNotifyErrorCalled = false; + private void setPerformNotifyCalled() { + synchronized (notifyMonitor) { + performNotifyCalled = true; + } + } + private void setPerformNotifyErrorCalled() { + synchronized (notifyMonitor) { + performNotifyErrorCalled = true; + } + } + private void resetNotifyCalled() { + synchronized (notifyMonitor) { + performNotifyCalled = false; + performNotifyErrorCalled = false; + } + } + private void assertBothCalled() { + synchronized (notifyMonitor) { + assertTrue(performNotifyCalled); + assertTrue(performNotifyErrorCalled); + } + } + private void assertErrorCalled() { + synchronized (notifyMonitor) { + assertFalse(performNotifyCalled); + assertTrue(performNotifyErrorCalled); + } + } + private void assertCalled() { + synchronized (notifyMonitor) { + assertTrue(performNotifyCalled); + assertFalse(performNotifyErrorCalled); + } + } + + public WaitHelper waitHelper; + + public TestWaitHelper() { + super(); + } + + public void setUp() { + WaitHelper.resetTestWaiter(); + waitHelper = WaitHelper.getTestWaiter(); + resetNotifyCalled(); + } + + public void tearDown() { + assertTrue(waitHelper.isIdle()); + } + + public Runnable performNothingRunnable() { + return new Runnable() { + public void run() { + } + }; + } + + public Runnable performNotifyRunnable() { + return new Runnable() { + public void run() { + setPerformNotifyCalled(); + waitHelper.performNotify(); + } + }; + } + + public Runnable performNotifyAfterDelayRunnable(final int delayInMillis) { + return new Runnable() { + public void run() { + try { + Thread.sleep(delayInMillis); + } catch (InterruptedException e) { + fail("Interrupted."); + } + + setPerformNotifyCalled(); + waitHelper.performNotify(); + } + }; + } + + public Runnable performNotifyErrorRunnable() { + return new Runnable() { + public void run() { + setPerformNotifyCalled(); + waitHelper.performNotify(new AssertionFailedError(ERROR_UNIQUE_IDENTIFIER)); + } + }; + } + + public Runnable inThreadPool(final Runnable runnable) { + return new Runnable() { + @Override + public void run() { + ThreadPool.run(runnable); + } + }; + } + + public Runnable inThread(final Runnable runnable) { + return new Runnable() { + @Override + public void run() { + new Thread(runnable).start(); + } + }; + } + + protected void expectAssertionFailedError(Runnable runnable) { + try { + waitHelper.performWait(runnable); + } catch (InnerError e) { + AssertionFailedError inner = (AssertionFailedError)e.innerError; + setPerformNotifyErrorCalled(); + String message = inner.getMessage(); + assertTrue("Expected '" + message + "' to contain '" + ERROR_UNIQUE_IDENTIFIER + "'", + message.contains(ERROR_UNIQUE_IDENTIFIER)); + } + } + + protected void expectAssertionFailedErrorAfterDelay(int wait, Runnable runnable) { + try { + waitHelper.performWait(wait, runnable); + } catch (InnerError e) { + AssertionFailedError inner = (AssertionFailedError)e.innerError; + setPerformNotifyErrorCalled(); + String message = inner.getMessage(); + assertTrue("Expected '" + message + "' to contain '" + ERROR_UNIQUE_IDENTIFIER + "'", + message.contains(ERROR_UNIQUE_IDENTIFIER)); + } + } + + public void testPerformWait() { + waitHelper.performWait(performNotifyRunnable()); + assertCalled(); + } + + public void testPerformWaitInThread() { + waitHelper.performWait(inThread(performNotifyRunnable())); + assertCalled(); + } + + public void testPerformWaitInThreadPool() { + waitHelper.performWait(inThreadPool(performNotifyRunnable())); + assertCalled(); + } + + public void testPerformTimeoutWait() { + waitHelper.performWait(SHORT_WAIT, performNotifyRunnable()); + assertCalled(); + } + + public void testPerformTimeoutWaitInThread() { + waitHelper.performWait(SHORT_WAIT, inThread(performNotifyRunnable())); + assertCalled(); + } + + public void testPerformTimeoutWaitInThreadPool() { + waitHelper.performWait(SHORT_WAIT, inThreadPool(performNotifyRunnable())); + assertCalled(); + } + + public void testPerformErrorWaitInThread() { + expectAssertionFailedError(inThread(performNotifyErrorRunnable())); + assertBothCalled(); + } + + public void testPerformErrorWaitInThreadPool() { + expectAssertionFailedError(inThreadPool(performNotifyErrorRunnable())); + assertBothCalled(); + } + + public void testPerformErrorTimeoutWaitInThread() { + expectAssertionFailedErrorAfterDelay(SHORT_WAIT, inThread(performNotifyErrorRunnable())); + assertBothCalled(); + } + + public void testPerformErrorTimeoutWaitInThreadPool() { + expectAssertionFailedErrorAfterDelay(SHORT_WAIT, inThreadPool(performNotifyErrorRunnable())); + assertBothCalled(); + } + + public void testTimeout() { + try { + waitHelper.performWait(SHORT_WAIT, performNothingRunnable()); + } catch (TimeoutError e) { + setPerformNotifyErrorCalled(); + assertEquals(SHORT_WAIT, e.waitTimeInMillis); + } + assertErrorCalled(); + } + + /** + * This will pass. The sequence in the main thread is: + * - A short delay. + * - performNotify is called. + * - performWait is called and immediately finds that performNotify was called before. + */ + public void testDelay() { + try { + waitHelper.performWait(1, performNotifyAfterDelayRunnable(SHORT_WAIT)); + } catch (AssertionFailedError e) { + setPerformNotifyErrorCalled(); + assertTrue(e.getMessage(), e.getMessage().contains("TIMEOUT")); + } + assertCalled(); + } + + public Runnable performNotifyMultipleTimesRunnable() { + return new Runnable() { + public void run() { + waitHelper.performNotify(); + setPerformNotifyCalled(); + waitHelper.performNotify(); + } + }; + } + + public void testPerformNotifyMultipleTimesFails() { + try { + waitHelper.performWait(NO_WAIT, performNotifyMultipleTimesRunnable()); // Not run on thread, so runnable executes before performWait looks for notifications. + } catch (WaitHelper.MultipleNotificationsError e) { + setPerformNotifyErrorCalled(); + } + assertBothCalled(); + assertFalse(waitHelper.isIdle()); // First perform notify should be hanging around. + waitHelper.performWait(NO_WAIT, performNothingRunnable()); + } + + public void testNestedWaitsAndNotifies() { + waitHelper.performWait(new Runnable() { + @Override + public void run() { + waitHelper.performWait(new Runnable() { + public void run() { + setPerformNotifyCalled(); + waitHelper.performNotify(); + } + }); + setPerformNotifyErrorCalled(); + waitHelper.performNotify(); + } + }); + assertBothCalled(); + } + + public void testAssertIsReported() { + try { + waitHelper.performWait(1, new Runnable() { + @Override + public void run() { + assertTrue("unique identifier", false); + } + }); + } catch (AssertionFailedError e) { + setPerformNotifyErrorCalled(); + assertTrue(e.getMessage(), e.getMessage().contains("unique identifier")); + } + assertErrorCalled(); + } + + /** + * The inner wait will timeout, but the outer wait will succeed. The sequence in the helper thread is: + * - A short delay. + * - performNotify is called. + * + * The sequence in the main thread is: + * - performWait is called and times out because the helper thread does not call + * performNotify quickly enough. + */ + public void testDelayInThread() throws InterruptedException { + waitHelper.performWait(new Runnable() { + @Override + public void run() { + try { + waitHelper.performWait(NO_WAIT, inThread(new Runnable() { + public void run() { + try { + Thread.sleep(SHORT_WAIT); + } catch (InterruptedException e) { + fail("Interrupted."); + } + + setPerformNotifyCalled(); + waitHelper.performNotify(); + } + })); + } catch (WaitHelper.TimeoutError e) { + setPerformNotifyErrorCalled(); + assertEquals(NO_WAIT, e.waitTimeInMillis); + } + } + }); + assertBothCalled(); + } + + /** + * The inner wait will timeout, but the outer wait will succeed. The sequence in the helper thread is: + * - A short delay. + * - performNotify is called. + * + * The sequence in the main thread is: + * - performWait is called and times out because the helper thread does not call + * performNotify quickly enough. + */ + public void testDelayInThreadPool() throws InterruptedException { + waitHelper.performWait(new Runnable() { + @Override + public void run() { + try { + waitHelper.performWait(NO_WAIT, inThreadPool(new Runnable() { + public void run() { + try { + Thread.sleep(SHORT_WAIT); + } catch (InterruptedException e) { + fail("Interrupted."); + } + + setPerformNotifyCalled(); + waitHelper.performNotify(); + } + })); + } catch (WaitHelper.TimeoutError e) { + setPerformNotifyErrorCalled(); + assertEquals(NO_WAIT, e.waitTimeInMillis); + } + } + }); + assertBothCalled(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java new file mode 100644 index 000000000..da980735b --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java @@ -0,0 +1,818 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import java.util.concurrent.ExecutorService; + +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.background.sync.helpers.DefaultBeginDelegate; +import org.mozilla.gecko.background.sync.helpers.DefaultCleanDelegate; +import org.mozilla.gecko.background.sync.helpers.DefaultFetchDelegate; +import org.mozilla.gecko.background.sync.helpers.DefaultFinishDelegate; +import org.mozilla.gecko.background.sync.helpers.DefaultSessionCreationDelegate; +import org.mozilla.gecko.background.sync.helpers.DefaultStoreDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectBeginDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectBeginFailDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectFinishDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectFinishFailDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectInvalidRequestFetchDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectManyStoredDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectStoreCompletedDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectStoredDelegate; +import org.mozilla.gecko.background.sync.helpers.SessionTestHelper; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +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.InvalidSessionTransitionException; +import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; +import org.mozilla.gecko.sync.repositories.Repository; +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import android.content.ContentValues; +import android.content.Context; + +public abstract class AndroidBrowserRepositoryTestCase extends AndroidSyncTestCase { + protected static String LOG_TAG = "BrowserRepositoryTest"; + + protected static void wipe(AndroidBrowserRepositoryDataAccessor helper) { + Logger.debug(LOG_TAG, "Wiping."); + try { + helper.wipe(); + } catch (NullPointerException e) { + // This will be handled in begin, here we can just ignore + // the error if it actually occurs since this is just test + // code. We will throw a ProfileDatabaseException. This + // error shouldn't occur in the future, but results from + // trying to access content providers before Fennec has + // been run at least once. + Logger.error(LOG_TAG, "ProfileDatabaseException seen in wipe. Begin should fail"); + fail("NullPointerException in wipe."); + } + } + + @Override + public void setUp() { + AndroidBrowserRepositoryDataAccessor helper = getDataAccessor(); + wipe(helper); + assertTrue(WaitHelper.getTestWaiter().isIdle()); + closeDataAccessor(helper); + } + + public void tearDown() { + assertTrue(WaitHelper.getTestWaiter().isIdle()); + } + + protected RepositorySession createSession() { + return SessionTestHelper.createSession( + getApplicationContext(), + getRepository()); + } + + protected RepositorySession createAndBeginSession() { + return SessionTestHelper.createAndBeginSession( + getApplicationContext(), + getRepository()); + } + + protected static void dispose(RepositorySession session) { + if (session != null) { + session.abort(); + } + } + + /** + * Hook to return an ExpectFetchDelegate, possibly with special GUIDs ignored. + */ + public ExpectFetchDelegate preparedExpectFetchDelegate(Record[] expected) { + return new ExpectFetchDelegate(expected); + } + + /** + * Hook to return an ExpectGuidsSinceDelegate, possibly with special GUIDs ignored. + */ + public ExpectGuidsSinceDelegate preparedExpectGuidsSinceDelegate(String[] expected) { + return new ExpectGuidsSinceDelegate(expected); + } + + /** + * Hook to return an ExpectGuidsSinceDelegate expecting only special GUIDs (if there are any). + */ + public ExpectGuidsSinceDelegate preparedExpectOnlySpecialGuidsSinceDelegate() { + return new ExpectGuidsSinceDelegate(new String[] {}); + } + + /** + * Hook to return an ExpectFetchSinceDelegate, possibly with special GUIDs ignored. + */ + public ExpectFetchSinceDelegate preparedExpectFetchSinceDelegate(long timestamp, String[] expected) { + return new ExpectFetchSinceDelegate(timestamp, expected); + } + + public static Runnable storeRunnable(final RepositorySession session, final Record record, final DefaultStoreDelegate delegate) { + return new Runnable() { + @Override + public void run() { + session.setStoreDelegate(delegate); + try { + session.store(record); + session.storeDone(); + } catch (NoStoreDelegateException e) { + fail("NoStoreDelegateException should not occur."); + } + } + }; + } + + public static Runnable storeRunnable(final RepositorySession session, final Record record) { + return storeRunnable(session, record, new ExpectStoredDelegate(record.guid)); + } + + public static Runnable storeManyRunnable(final RepositorySession session, final Record[] records, final DefaultStoreDelegate delegate) { + return new Runnable() { + @Override + public void run() { + session.setStoreDelegate(delegate); + try { + for (Record record : records) { + session.store(record); + } + session.storeDone(); + } catch (NoStoreDelegateException e) { + fail("NoStoreDelegateException should not occur."); + } + } + }; + } + + public static Runnable storeManyRunnable(final RepositorySession session, final Record[] records) { + return storeManyRunnable(session, records, new ExpectManyStoredDelegate(records)); + } + + /** + * Store a record and don't expect a store callback until we're done. + * + * @param session + * @param record + * @return Runnable. + */ + public static Runnable quietStoreRunnable(final RepositorySession session, final Record record) { + return storeRunnable(session, record, new ExpectStoreCompletedDelegate()); + } + + public static Runnable beginRunnable(final RepositorySession session, final DefaultBeginDelegate delegate) { + return new Runnable() { + @Override + public void run() { + try { + session.begin(delegate); + } catch (InvalidSessionTransitionException e) { + performNotify(e); + } + } + }; + } + + public static Runnable finishRunnable(final RepositorySession session, final DefaultFinishDelegate delegate) { + return new Runnable() { + @Override + public void run() { + try { + session.finish(delegate); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + }; + } + + public static Runnable fetchAllRunnable(final RepositorySession session, final ExpectFetchDelegate delegate) { + return new Runnable() { + @Override + public void run() { + session.fetchAll(delegate); + } + }; + } + + public Runnable fetchAllRunnable(final RepositorySession session, final Record[] expectedRecords) { + return fetchAllRunnable(session, preparedExpectFetchDelegate(expectedRecords)); + } + + public Runnable guidsSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) { + return new Runnable() { + @Override + public void run() { + session.guidsSince(timestamp, preparedExpectGuidsSinceDelegate(expected)); + } + }; + } + + public Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) { + return new Runnable() { + @Override + public void run() { + session.fetchSince(timestamp, preparedExpectFetchSinceDelegate(timestamp, expected)); + } + }; + } + + public static Runnable fetchRunnable(final RepositorySession session, final String[] guids, final DefaultFetchDelegate delegate) { + return new Runnable() { + @Override + public void run() { + try { + session.fetch(guids, delegate); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + }; + } + public Runnable fetchRunnable(final RepositorySession session, final String[] guids, final Record[] expected) { + return fetchRunnable(session, guids, preparedExpectFetchDelegate(expected)); + } + + public static Runnable cleanRunnable(final Repository repository, final boolean success, final Context context, final DefaultCleanDelegate delegate) { + return new Runnable() { + @Override + public void run() { + repository.clean(success, delegate, context); + } + }; + } + + protected abstract Repository getRepository(); + protected abstract AndroidBrowserRepositoryDataAccessor getDataAccessor(); + + protected static void doStore(RepositorySession session, Record[] records) { + performWait(storeManyRunnable(session, records)); + } + + // Tests to implement + public abstract void testFetchAll(); + public abstract void testGuidsSinceReturnMultipleRecords(); + public abstract void testGuidsSinceReturnNoRecords(); + public abstract void testFetchSinceOneRecord(); + public abstract void testFetchSinceReturnNoRecords(); + public abstract void testFetchOneRecordByGuid(); + public abstract void testFetchMultipleRecordsByGuids(); + public abstract void testFetchNoRecordByGuid(); + public abstract void testWipe(); + public abstract void testStore(); + public abstract void testRemoteNewerTimeStamp(); + public abstract void testLocalNewerTimeStamp(); + public abstract void testDeleteRemoteNewer(); + public abstract void testDeleteLocalNewer(); + public abstract void testDeleteRemoteLocalNonexistent(); + public abstract void testStoreIdenticalExceptGuid(); + public abstract void testCleanMultipleRecords(); + + + /* + * Test abstractions + */ + protected void basicStoreTest(Record record) { + final RepositorySession session = createAndBeginSession(); + performWait(storeRunnable(session, record)); + } + + protected void basicFetchAllTest(Record[] expected) { + Logger.debug("rnewman", "Starting testFetchAll."); + RepositorySession session = createAndBeginSession(); + Logger.debug("rnewman", "Prepared."); + + AndroidBrowserRepositoryDataAccessor helper = getDataAccessor(); + helper.dumpDB(); + performWait(storeManyRunnable(session, expected)); + + helper.dumpDB(); + performWait(fetchAllRunnable(session, expected)); + + closeDataAccessor(helper); + dispose(session); + } + + /* + * Tests for clean + */ + // Input: 4 records; 2 which are to be cleaned, 2 which should remain after the clean + protected void cleanMultipleRecords(Record delete0, Record delete1, Record keep0, Record keep1, Record keep2) { + RepositorySession session = createAndBeginSession(); + doStore(session, new Record[] { + delete0, delete1, keep0, keep1, keep2 + }); + + // Force two records to appear deleted. + AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); + ContentValues cv = new ContentValues(); + cv.put(BrowserContract.SyncColumns.IS_DELETED, 1); + db.updateByGuid(delete0.guid, cv); + db.updateByGuid(delete1.guid, cv); + + final DefaultCleanDelegate delegate = new DefaultCleanDelegate() { + public void onCleaned(Repository repo) { + performNotify(); + } + }; + + final Runnable cleanRunnable = cleanRunnable( + getRepository(), + true, + getApplicationContext(), + delegate); + + performWait(cleanRunnable); + performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(new Record[] { keep0, keep1, keep2}))); + closeDataAccessor(db); + dispose(session); + } + + /* + * Tests for guidsSince + */ + protected void guidsSinceReturnMultipleRecords(Record record0, Record record1) { + RepositorySession session = createAndBeginSession(); + long timestamp = System.currentTimeMillis(); + + String[] expected = new String[2]; + expected[0] = record0.guid; + expected[1] = record1.guid; + + Logger.debug(getName(), "Storing two records..."); + performWait(storeManyRunnable(session, new Record[] { record0, record1 })); + Logger.debug(getName(), "Getting guids since " + timestamp + "; expecting " + expected.length); + performWait(guidsSinceRunnable(session, timestamp, expected)); + dispose(session); + } + + protected void guidsSinceReturnNoRecords(Record record0) { + RepositorySession session = createAndBeginSession(); + + // Store 1 record in the past. + performWait(storeRunnable(session, record0)); + + String[] expected = {}; + performWait(guidsSinceRunnable(session, System.currentTimeMillis() + 1000, expected)); + dispose(session); + } + + /* + * Tests for fetchSince + */ + protected void fetchSinceOneRecord(Record record0, Record record1) { + RepositorySession session = createAndBeginSession(); + + performWait(storeRunnable(session, record0)); + long timestamp = System.currentTimeMillis(); + Logger.debug("fetchSinceOneRecord", "Entering synchronized section. Timestamp " + timestamp); + synchronized(this) { + try { + wait(1000); + } catch (InterruptedException e) { + Logger.warn("fetchSinceOneRecord", "Interrupted.", e); + } + } + Logger.debug("fetchSinceOneRecord", "Storing."); + performWait(storeRunnable(session, record1)); + + Logger.debug("fetchSinceOneRecord", "Fetching record 1."); + String[] expectedOne = new String[] { record1.guid }; + performWait(fetchSinceRunnable(session, timestamp + 10, expectedOne)); + + Logger.debug("fetchSinceOneRecord", "Fetching both, relying on inclusiveness."); + String[] expectedBoth = new String[] { record0.guid, record1.guid }; + performWait(fetchSinceRunnable(session, timestamp - 3000, expectedBoth)); + + Logger.debug("fetchSinceOneRecord", "Done."); + dispose(session); + } + + protected void fetchSinceReturnNoRecords(Record record) { + RepositorySession session = createAndBeginSession(); + + performWait(storeRunnable(session, record)); + + long timestamp = System.currentTimeMillis(); + + performWait(fetchSinceRunnable(session, timestamp + 2000, new String[] {})); + dispose(session); + } + + protected void fetchOneRecordByGuid(Record record0, Record record1) { + RepositorySession session = createAndBeginSession(); + + Record[] store = new Record[] { record0, record1 }; + performWait(storeManyRunnable(session, store)); + + String[] guids = new String[] { record0.guid }; + Record[] expected = new Record[] { record0 }; + performWait(fetchRunnable(session, guids, expected)); + dispose(session); + } + + protected void fetchMultipleRecordsByGuids(Record record0, + Record record1, Record record2) { + RepositorySession session = createAndBeginSession(); + + Record[] store = new Record[] { record0, record1, record2 }; + performWait(storeManyRunnable(session, store)); + + String[] guids = new String[] { record0.guid, record2.guid }; + Record[] expected = new Record[] { record0, record2 }; + performWait(fetchRunnable(session, guids, expected)); + dispose(session); + } + + protected void fetchNoRecordByGuid(Record record) { + RepositorySession session = createAndBeginSession(); + + performWait(storeRunnable(session, record)); + performWait(fetchRunnable(session, + new String[] { Utils.generateGuid() }, + new Record[] {})); + dispose(session); + } + + /* + * Test wipe + */ + protected void doWipe(final Record record0, final Record record1) { + final RepositorySession session = createAndBeginSession(); + final Runnable run = new Runnable() { + @Override + public void run() { + session.wipe(new RepositorySessionWipeDelegate() { + public void onWipeSucceeded() { + performNotify(); + } + public void onWipeFailed(Exception ex) { + fail("wipe should have succeeded"); + performNotify(); + } + @Override + public RepositorySessionWipeDelegate deferredWipeDelegate(final ExecutorService executor) { + final RepositorySessionWipeDelegate self = this; + return new RepositorySessionWipeDelegate() { + + @Override + public void onWipeSucceeded() { + new Thread(new Runnable() { + @Override + public void run() { + self.onWipeSucceeded(); + }}).start(); + } + + @Override + public void onWipeFailed(final Exception ex) { + new Thread(new Runnable() { + @Override + public void run() { + self.onWipeFailed(ex); + }}).start(); + } + + @Override + public RepositorySessionWipeDelegate deferredWipeDelegate(ExecutorService newExecutor) { + if (newExecutor == executor) { + return this; + } + throw new IllegalArgumentException("Can't re-defer this delegate."); + } + }; + } + }); + } + }; + + // Store 2 records. + Record[] records = new Record[] { record0, record1 }; + performWait(storeManyRunnable(session, records)); + performWait(fetchAllRunnable(session, records)); + + // Wipe. + performWait(run); + dispose(session); + } + + /* + * TODO adding or subtracting from lastModified timestamps does NOTHING + * since it gets overwritten when we store stuff. See other tests + * for ways to do this properly. + */ + + /* + * Record being stored has newer timestamp than existing local record, local + * record has not been modified since last sync. + */ + protected void remoteNewerTimeStamp(Record local, Record remote) { + final RepositorySession session = createAndBeginSession(); + + // Record existing and hasn't changed since before lastSync. + // Automatically will be assigned lastModified = current time. + performWait(storeRunnable(session, local)); + + remote.guid = local.guid; + + // Get the timestamp and make remote newer than it + ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { local }); + performWait(fetchRunnable(session, new String[] { remote.guid }, timestampDelegate)); + remote.lastModified = timestampDelegate.records.get(0).lastModified + 1000; + performWait(storeRunnable(session, remote)); + + Record[] expected = new Record[] { remote }; + ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected); + performWait(fetchAllRunnable(session, delegate)); + dispose(session); + } + + /* + * Local record has a newer timestamp than the record being stored. For now, + * we just take newer (local) record) + */ + protected void localNewerTimeStamp(Record local, Record remote) { + final RepositorySession session = createAndBeginSession(); + + performWait(storeRunnable(session, local)); + + remote.guid = local.guid; + + // Get the timestamp and make remote older than it + ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { local }); + performWait(fetchRunnable(session, new String[] { remote.guid }, timestampDelegate)); + remote.lastModified = timestampDelegate.records.get(0).lastModified - 1000; + performWait(storeRunnable(session, remote)); + + // Do a fetch and make sure that we get back the local record. + Record[] expected = new Record[] { local }; + performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(expected))); + dispose(session); + } + + /* + * Insert a record that is marked as deleted, remote has newer timestamp + */ + protected void deleteRemoteNewer(Record local, Record remote) { + final RepositorySession session = createAndBeginSession(); + + // Record existing and hasn't changed since before lastSync. + // Automatically will be assigned lastModified = current time. + performWait(storeRunnable(session, local)); + + // Pass the same record to store, but mark it deleted and modified + // more recently + ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { local }); + performWait(fetchRunnable(session, new String[] { local.guid }, timestampDelegate)); + remote.lastModified = timestampDelegate.records.get(0).lastModified + 1000; + remote.deleted = true; + remote.guid = local.guid; + performWait(storeRunnable(session, remote)); + + performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(new Record[]{}))); + dispose(session); + } + + // Store two records that are identical (this has different meanings based on the + // type of record) other than their guids. The record existing locally already + // should have its guid replaced (the assumption is that the record existed locally + // and then sync was enabled and this record existed on another sync'd device). + public void storeIdenticalExceptGuid(Record record0) { + Logger.debug("storeIdenticalExceptGuid", "Started."); + final RepositorySession session = createAndBeginSession(); + Logger.debug("storeIdenticalExceptGuid", "Session is " + session); + performWait(storeRunnable(session, record0)); + Logger.debug("storeIdenticalExceptGuid", "Stored record0."); + DefaultFetchDelegate timestampDelegate = getTimestampDelegate(record0.guid); + + performWait(fetchRunnable(session, new String[] { record0.guid }, timestampDelegate)); + Logger.debug("storeIdenticalExceptGuid", "fetchRunnable done."); + record0.lastModified = timestampDelegate.records.get(0).lastModified + 3000; + record0.guid = Utils.generateGuid(); + Logger.debug("storeIdenticalExceptGuid", "Storing modified..."); + performWait(storeRunnable(session, record0)); + Logger.debug("storeIdenticalExceptGuid", "Stored modified."); + + Record[] expected = new Record[] { record0 }; + performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(expected))); + Logger.debug("storeIdenticalExceptGuid", "Fetched all. Returning."); + dispose(session); + } + + // Special delegate so that we don't verify parenting is correct since + // at some points it won't be since parent folder hasn't been stored. + private DefaultFetchDelegate getTimestampDelegate(final String guid) { + return new DefaultFetchDelegate() { + @Override + public void onFetchCompleted(final long fetchEnd) { + assertEquals(guid, this.records.get(0).guid); + performNotify(); + } + }; + } + + /* + * Insert a record that is marked as deleted, local has newer timestamp + * and was not marked deleted (so keep it) + */ + protected void deleteLocalNewer(Record local, Record remote) { + Logger.debug("deleteLocalNewer", "Begin."); + final RepositorySession session = createAndBeginSession(); + + Logger.debug("deleteLocalNewer", "Storing local..."); + performWait(storeRunnable(session, local)); + + // Create an older version of a record with the same GUID. + remote.guid = local.guid; + + Logger.debug("deleteLocalNewer", "Fetching..."); + + // Get the timestamp and make remote older than it + Record[] expected = new Record[] { local }; + ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(expected); + performWait(fetchRunnable(session, new String[] { remote.guid }, timestampDelegate)); + + Logger.debug("deleteLocalNewer", "Fetched."); + remote.lastModified = timestampDelegate.records.get(0).lastModified - 1000; + + Logger.debug("deleteLocalNewer", "Last modified is " + remote.lastModified); + remote.deleted = true; + Logger.debug("deleteLocalNewer", "Storing deleted..."); + performWait(quietStoreRunnable(session, remote)); // This appears to do a lot of work...?! + + // Do a fetch and make sure that we get back the first (local) record. + performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(expected))); + Logger.debug("deleteLocalNewer", "Fetched and done!"); + dispose(session); + } + + /* + * Insert a record that is marked as deleted, record never existed locally + */ + protected void deleteRemoteLocalNonexistent(Record remote) { + final RepositorySession session = createAndBeginSession(); + + long timestamp = 1000000000; + + // Pass a record marked deleted to store, doesn't exist locally + remote.lastModified = timestamp; + remote.deleted = true; + performWait(quietStoreRunnable(session, remote)); + + ExpectFetchDelegate delegate = preparedExpectFetchDelegate(new Record[]{}); + performWait(fetchAllRunnable(session, delegate)); + dispose(session); + } + + /* + * Tests that don't require specific records based on type of repository. + * These tests don't need to be overriden in subclasses, they will just work. + */ + public void testCreateSessionNullContext() { + Logger.debug(LOG_TAG, "In testCreateSessionNullContext."); + Repository repo = getRepository(); + try { + repo.createSession(new DefaultSessionCreationDelegate(), null); + fail("Should throw."); + } catch (Exception ex) { + assertNotNull(ex); + } + } + + public void testStoreNullRecord() { + final RepositorySession session = createAndBeginSession(); + try { + session.setStoreDelegate(new DefaultStoreDelegate()); + session.store(null); + fail("Should throw."); + } catch (Exception ex) { + assertNotNull(ex); + } + dispose(session); + } + + public void testFetchNoGuids() { + final RepositorySession session = createAndBeginSession(); + performWait(fetchRunnable(session, new String[] {}, new ExpectInvalidRequestFetchDelegate())); + dispose(session); + } + + public void testFetchNullGuids() { + final RepositorySession session = createAndBeginSession(); + performWait(fetchRunnable(session, null, new ExpectInvalidRequestFetchDelegate())); + dispose(session); + } + + public void testBeginOnNewSession() { + final RepositorySession session = createSession(); + performWait(beginRunnable(session, new ExpectBeginDelegate())); + dispose(session); + } + + public void testBeginOnRunningSession() { + final RepositorySession session = createAndBeginSession(); + try { + session.begin(new ExpectBeginFailDelegate()); + } catch (InvalidSessionTransitionException e) { + dispose(session); + return; + } + fail("Should have caught InvalidSessionTransitionException."); + } + + public void testBeginOnFinishedSession() throws InactiveSessionException { + final RepositorySession session = createAndBeginSession(); + performWait(finishRunnable(session, new ExpectFinishDelegate())); + try { + session.begin(new ExpectBeginFailDelegate()); + } catch (InvalidSessionTransitionException e) { + Logger.debug(getName(), "Yay! Got an exception.", e); + dispose(session); + return; + } catch (Exception e) { + Logger.debug(getName(), "Yay! Got an exception.", e); + dispose(session); + return; + } + fail("Should have caught InvalidSessionTransitionException."); + } + + public void testFinishOnFinishedSession() throws InactiveSessionException { + final RepositorySession session = createAndBeginSession(); + performWait(finishRunnable(session, new ExpectFinishDelegate())); + try { + session.finish(new ExpectFinishFailDelegate()); + } catch (InactiveSessionException e) { + dispose(session); + return; + } + fail("Should have caught InactiveSessionException."); + } + + public void testFetchOnInactiveSession() throws InactiveSessionException { + final RepositorySession session = createSession(); + try { + session.fetch(new String[] { Utils.generateGuid() }, new DefaultFetchDelegate()); + } catch (InactiveSessionException e) { + // Yay. + dispose(session); + return; + }; + fail("Should have caught InactiveSessionException."); + } + + public void testFetchOnFinishedSession() { + final RepositorySession session = createAndBeginSession(); + Logger.debug(getName(), "Finishing..."); + performWait(finishRunnable(session, new ExpectFinishDelegate())); + try { + session.fetch(new String[] { Utils.generateGuid() }, new DefaultFetchDelegate()); + } catch (InactiveSessionException e) { + // Yay. + dispose(session); + return; + }; + fail("Should have caught InactiveSessionException."); + } + + public void testGuidsSinceOnUnstartedSession() { + final RepositorySession session = createSession(); + Runnable run = new Runnable() { + @Override + public void run() { + session.guidsSince(System.currentTimeMillis(), + new RepositorySessionGuidsSinceDelegate() { + public void onGuidsSinceSucceeded(String[] guids) { + fail("Session inactive, should fail"); + performNotify(); + } + + public void onGuidsSinceFailed(Exception ex) { + verifyInactiveException(ex); + performNotify(); + } + }); + } + }; + performWait(run); + dispose(session); + } + + private static void verifyInactiveException(Exception ex) { + if (!(ex instanceof InactiveSessionException)) { + fail("Wrong exception type"); + } + } + + protected void closeDataAccessor(AndroidBrowserRepositoryDataAccessor dataAccessor) { + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java new file mode 100644 index 000000000..71563a46c --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java @@ -0,0 +1,636 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import java.util.ArrayList; + +import org.json.simple.JSONArray; +import org.mozilla.gecko.background.sync.helpers.BookmarkHelpers; +import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectFinishDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectInvalidTypeStoreDelegate; +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.RepositorySession; +import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksDataAccessor; +import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository; +import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepositorySession; +import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepository; +import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor; +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.BookmarkRecord; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +public class TestAndroidBrowserBookmarksRepository extends AndroidBrowserRepositoryTestCase { + + @Override + protected AndroidBrowserRepository getRepository() { + + /** + * Override this chain in order to avoid our test code having to create two + * sessions all the time. + */ + return new AndroidBrowserBookmarksRepository() { + @Override + protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) { + AndroidBrowserBookmarksRepositorySession session; + session = new AndroidBrowserBookmarksRepositorySession(this, context) { + @Override + protected synchronized void trackGUID(String guid) { + System.out.println("Ignoring trackGUID call: this is a test!"); + } + }; + delegate.deferredCreationDelegate().onSessionCreated(session); + } + }; + } + + @Override + protected AndroidBrowserRepositoryDataAccessor getDataAccessor() { + return new AndroidBrowserBookmarksDataAccessor(getApplicationContext()); + } + + /** + * Hook to return an ExpectFetchDelegate, possibly with special GUIDs ignored. + */ + @Override + public ExpectFetchDelegate preparedExpectFetchDelegate(Record[] expected) { + ExpectFetchDelegate delegate = new ExpectFetchDelegate(expected); + delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet()); + return delegate; + } + + /** + * Hook to return an ExpectGuidsSinceDelegate expecting only special GUIDs (if there are any). + */ + public ExpectGuidsSinceDelegate preparedExpectOnlySpecialGuidsSinceDelegate() { + ExpectGuidsSinceDelegate delegate = new ExpectGuidsSinceDelegate(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet().toArray(new String[] {})); + return delegate; + } + + /** + * Hook to return an ExpectGuidsSinceDelegate, possibly with special GUIDs ignored. + */ + @Override + public ExpectGuidsSinceDelegate preparedExpectGuidsSinceDelegate(String[] expected) { + ExpectGuidsSinceDelegate delegate = new ExpectGuidsSinceDelegate(expected); + delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet()); + return delegate; + } + + /** + * Hook to return an ExpectFetchSinceDelegate, possibly with special GUIDs ignored. + */ + public ExpectFetchSinceDelegate preparedExpectFetchSinceDelegate(long timestamp, String[] expected) { + ExpectFetchSinceDelegate delegate = new ExpectFetchSinceDelegate(timestamp, expected); + delegate.ignore.addAll(AndroidBrowserBookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet()); + return delegate; + } + + // NOTE NOTE NOTE + // Must store folder before records if we we are checking that the + // records returned are the same as those sent in. If the folder isn't stored + // first, the returned records won't be identical to those stored because we + // aren't able to find the parent name/guid when we do a fetch. If you don't want + // to store a folder first, store your record in "mobile" or one of the folders + // that always exists. + + public void testFetchOneWithChildren() { + BookmarkRecord folder = BookmarkHelpers.createFolder1(); + BookmarkRecord bookmark1 = BookmarkHelpers.createBookmark1(); + BookmarkRecord bookmark2 = BookmarkHelpers.createBookmark2(); + + RepositorySession session = createAndBeginSession(); + + Record[] records = new Record[] { folder, bookmark1, bookmark2 }; + performWait(storeManyRunnable(session, records)); + + AndroidBrowserRepositoryDataAccessor helper = getDataAccessor(); + helper.dumpDB(); + closeDataAccessor(helper); + + String[] guids = new String[] { folder.guid }; + Record[] expected = new Record[] { folder }; + performWait(fetchRunnable(session, guids, expected)); + dispose(session); + } + + @Override + public void testFetchAll() { + Record[] expected = new Record[3]; + expected[0] = BookmarkHelpers.createFolder1(); + expected[1] = BookmarkHelpers.createBookmark1(); + expected[2] = BookmarkHelpers.createBookmark2(); + basicFetchAllTest(expected); + } + + @Override + public void testGuidsSinceReturnMultipleRecords() { + BookmarkRecord record0 = BookmarkHelpers.createBookmark1(); + BookmarkRecord record1 = BookmarkHelpers.createBookmark2(); + guidsSinceReturnMultipleRecords(record0, record1); + } + + @Override + public void testGuidsSinceReturnNoRecords() { + guidsSinceReturnNoRecords(BookmarkHelpers.createBookmarkInMobileFolder1()); + } + + @Override + public void testFetchSinceOneRecord() { + fetchSinceOneRecord(BookmarkHelpers.createBookmarkInMobileFolder1(), + BookmarkHelpers.createBookmarkInMobileFolder2()); + } + + @Override + public void testFetchSinceReturnNoRecords() { + fetchSinceReturnNoRecords(BookmarkHelpers.createBookmark1()); + } + + @Override + public void testFetchOneRecordByGuid() { + fetchOneRecordByGuid(BookmarkHelpers.createBookmarkInMobileFolder1(), + BookmarkHelpers.createBookmarkInMobileFolder2()); + } + + @Override + public void testFetchMultipleRecordsByGuids() { + BookmarkRecord record0 = BookmarkHelpers.createFolder1(); + BookmarkRecord record1 = BookmarkHelpers.createBookmark1(); + BookmarkRecord record2 = BookmarkHelpers.createBookmark2(); + fetchMultipleRecordsByGuids(record0, record1, record2); + } + + @Override + public void testFetchNoRecordByGuid() { + fetchNoRecordByGuid(BookmarkHelpers.createBookmark1()); + } + + + @Override + public void testWipe() { + doWipe(BookmarkHelpers.createBookmarkInMobileFolder1(), + BookmarkHelpers.createBookmarkInMobileFolder2()); + } + + @Override + public void testStore() { + basicStoreTest(BookmarkHelpers.createBookmark1()); + } + + + public void testStoreFolder() { + basicStoreTest(BookmarkHelpers.createFolder1()); + } + + /** + * TODO: 2011-12-24, tests disabled because we no longer fail + * a store call if we get an unknown record type. + */ + /* + * Test storing each different type of Bookmark record. + * We expect any records with type other than "bookmark" + * or "folder" to fail. For now we throw these away. + */ + /* + public void testStoreMicrosummary() { + basicStoreFailTest(BookmarkHelpers.createMicrosummary()); + } + + public void testStoreQuery() { + basicStoreFailTest(BookmarkHelpers.createQuery()); + } + + public void testStoreLivemark() { + basicStoreFailTest(BookmarkHelpers.createLivemark()); + } + + public void testStoreSeparator() { + basicStoreFailTest(BookmarkHelpers.createSeparator()); + } + */ + + protected void basicStoreFailTest(Record record) { + final RepositorySession session = createAndBeginSession(); + performWait(storeRunnable(session, record, new ExpectInvalidTypeStoreDelegate())); + dispose(session); + } + + /* + * Re-parenting tests + */ + // Insert two records missing parent, then insert their parent. + // Make sure they end up with the correct parent on fetch. + public void testBasicReparenting() throws InactiveSessionException { + Record[] expected = new Record[] { + BookmarkHelpers.createBookmark1(), + BookmarkHelpers.createBookmark2(), + BookmarkHelpers.createFolder1() + }; + doMultipleFolderReparentingTest(expected); + } + + // Insert 3 folders and 4 bookmarks in different orders + // and make sure they come out parented correctly + public void testMultipleFolderReparenting1() throws InactiveSessionException { + Record[] expected = new Record[] { + BookmarkHelpers.createBookmark1(), + BookmarkHelpers.createBookmark2(), + BookmarkHelpers.createBookmark3(), + BookmarkHelpers.createFolder1(), + BookmarkHelpers.createBookmark4(), + BookmarkHelpers.createFolder3(), + BookmarkHelpers.createFolder2(), + }; + doMultipleFolderReparentingTest(expected); + } + + public void testMultipleFolderReparenting2() throws InactiveSessionException { + Record[] expected = new Record[] { + BookmarkHelpers.createBookmark1(), + BookmarkHelpers.createBookmark2(), + BookmarkHelpers.createBookmark3(), + BookmarkHelpers.createFolder1(), + BookmarkHelpers.createBookmark4(), + BookmarkHelpers.createFolder3(), + BookmarkHelpers.createFolder2(), + }; + doMultipleFolderReparentingTest(expected); + } + + public void testMultipleFolderReparenting3() throws InactiveSessionException { + Record[] expected = new Record[] { + BookmarkHelpers.createBookmark1(), + BookmarkHelpers.createBookmark2(), + BookmarkHelpers.createBookmark3(), + BookmarkHelpers.createFolder1(), + BookmarkHelpers.createBookmark4(), + BookmarkHelpers.createFolder3(), + BookmarkHelpers.createFolder2(), + }; + doMultipleFolderReparentingTest(expected); + } + + private void doMultipleFolderReparentingTest(Record[] expected) throws InactiveSessionException { + final RepositorySession session = createAndBeginSession(); + doStore(session, expected); + ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected); + performWait(fetchAllRunnable(session, delegate)); + performWait(finishRunnable(session, new ExpectFinishDelegate())); + } + + /* + * 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(BookmarkHelpers.createBookmarkInMobileFolder1()); + } + + /* + * More complicated situation in which we insert a folder + * followed by a couple of its children. We then insert + * the folder again but with a different guid. Children + * must still get correct parent when they are fetched. + * Store a record after with the new guid as the parent + * and make sure it works as well. + */ + public void testStoreIdenticalFoldersWithChildren() { + final RepositorySession session = createAndBeginSession(); + Record record0 = BookmarkHelpers.createFolder1(); + + // Get timestamp so that the conflicting folder that we store below is newer. + // Children won't come back on this fetch since they haven't been stored, so remove them + // before our delegate throws a failure. + BookmarkRecord rec0 = (BookmarkRecord) record0; + rec0.children = new JSONArray(); + performWait(storeRunnable(session, record0)); + + ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { rec0 }); + performWait(fetchRunnable(session, new String[] { record0.guid }, timestampDelegate)); + + AndroidBrowserRepositoryDataAccessor helper = getDataAccessor(); + helper.dumpDB(); + closeDataAccessor(helper); + + Record record1 = BookmarkHelpers.createBookmark1(); + Record record2 = BookmarkHelpers.createBookmark2(); + Record record3 = BookmarkHelpers.createFolder1(); + BookmarkRecord bmk3 = (BookmarkRecord) record3; + record3.guid = Utils.generateGuid(); + record3.lastModified = timestampDelegate.records.get(0).lastModified + 3000; + assertFalse(record0.guid.equals(record3.guid)); + + // Store an additional record after inserting the duplicate folder + // with new GUID. Make sure it comes back as well. + Record record4 = BookmarkHelpers.createBookmark3(); + BookmarkRecord bmk4 = (BookmarkRecord) record4; + bmk4.parentID = bmk3.guid; + bmk4.parentName = bmk3.parentName; + + doStore(session, new Record[] { + record1, record2, record3, bmk4 + }); + BookmarkRecord bmk1 = (BookmarkRecord) record1; + bmk1.parentID = record3.guid; + BookmarkRecord bmk2 = (BookmarkRecord) record2; + bmk2.parentID = record3.guid; + Record[] expect = new Record[] { + bmk1, bmk2, record3 + }; + fetchAllRunnable(session, preparedExpectFetchDelegate(expect)); + dispose(session); + } + + @Override + public void testRemoteNewerTimeStamp() { + BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); + BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); + remoteNewerTimeStamp(local, remote); + } + + @Override + public void testLocalNewerTimeStamp() { + BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); + BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); + localNewerTimeStamp(local, remote); + } + + @Override + public void testDeleteRemoteNewer() { + BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); + BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); + deleteRemoteNewer(local, remote); + } + + @Override + public void testDeleteLocalNewer() { + BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); + BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); + deleteLocalNewer(local, remote); + } + + @Override + public void testDeleteRemoteLocalNonexistent() { + BookmarkRecord remote = BookmarkHelpers.createBookmark2(); + deleteRemoteLocalNonexistent(remote); + } + + @Override + public void testCleanMultipleRecords() { + cleanMultipleRecords( + BookmarkHelpers.createBookmarkInMobileFolder1(), + BookmarkHelpers.createBookmarkInMobileFolder2(), + BookmarkHelpers.createBookmark1(), + BookmarkHelpers.createBookmark2(), + BookmarkHelpers.createFolder1()); + } + + public void testBasicPositioning() { + final RepositorySession session = createAndBeginSession(); + Record[] expected = new Record[] { + BookmarkHelpers.createBookmark1(), + BookmarkHelpers.createFolder1(), + BookmarkHelpers.createBookmark2() + }; + System.out.println("TEST: Inserting " + expected[0].guid + ", " + + expected[1].guid + ", " + + expected[2].guid); + doStore(session, expected); + + ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected); + performWait(fetchAllRunnable(session, delegate)); + + int found = 0; + boolean foundFolder = false; + for (int i = 0; i < delegate.records.size(); i++) { + BookmarkRecord rec = (BookmarkRecord) delegate.records.get(i); + if (rec.guid.equals(expected[0].guid)) { + assertEquals(0, ((BookmarkRecord) delegate.records.get(i)).androidPosition); + found++; + } else if (rec.guid.equals(expected[2].guid)) { + assertEquals(1, ((BookmarkRecord) delegate.records.get(i)).androidPosition); + found++; + } else if (rec.guid.equals(expected[1].guid)) { + foundFolder = true; + } else { + System.out.println("TEST: found " + rec.guid); + } + } + assertTrue(foundFolder); + assertEquals(2, found); + dispose(session); + } + + public void testSqlInjectPurgeDeleteAndUpdateByGuid() { + // Some setup. + RepositorySession session = createAndBeginSession(); + AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); + + ContentValues cv = new ContentValues(); + cv.put(BrowserContract.SyncColumns.IS_DELETED, 1); + + // Create and insert 2 bookmarks, 2nd one is evil (attempts injection). + BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); + BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); + bmk2.guid = "' or '1'='1"; + + db.insert(bmk1); + db.insert(bmk2); + + // Test 1 - updateByGuid() handles evil bookmarks correctly. + db.updateByGuid(bmk2.guid, cv); + + // Query bookmarks table. + Cursor cur = getAllBookmarks(); + int numBookmarks = cur.getCount(); + + // Ensure only the evil bookmark 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(bmk2.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 = getAllBookmarks(); + int numBookmarksAfterDeletion = cur.getCount(); + + // Ensure we have only 1 deleted row. + assertEquals(numBookmarksAfterDeletion, numBookmarks - 1); + + // Ensure only the evil bookmark 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(bmk2.guid)) { + fail("Evil guid was not deleted!"); + } else { + assertFalse(deleted); + } + cur.moveToNext(); + } + } finally { + cur.close(); + } + dispose(session); + } + + protected Cursor getAllBookmarks() { + Context context = getApplicationContext(); + Cursor cur = context.getContentResolver().query(BrowserContractHelpers.BOOKMARKS_CONTENT_URI, + BrowserContractHelpers.BookmarkColumns, null, null, null); + return cur; + } + + public void testSqlInjectFetch() { + // Some setup. + RepositorySession session = createAndBeginSession(); + AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); + + // Create and insert 4 bookmarks, last one is evil (attempts injection). + BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); + BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); + BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3(); + BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4(); + bmk4.guid = "' or '1'='1"; + + db.insert(bmk1); + db.insert(bmk2); + db.insert(bmk3); + db.insert(bmk4); + + // Perform a fetch. + Cursor cur = null; + try { + cur = db.fetch(new String[] { bmk3.guid, bmk4.guid }); + } catch (NullCursorException e1) { + e1.printStackTrace(); + } + + // Ensure the correct number (2) of records were fetched and with the correct guids. + if (cur == null) { + fail("No records were fetched."); + } + + try { + if (cur.getCount() != 2) { + fail("Wrong number of guids fetched!"); + } + cur.moveToFirst(); + while (!cur.isAfterLast()) { + String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); + if (!guid.equals(bmk3.guid) && !guid.equals(bmk4.guid)) { + fail("Wrong guids were fetched!"); + } + cur.moveToNext(); + } + } finally { + cur.close(); + } + dispose(session); + } + + public void testSqlInjectDelete() { + // Some setup. + RepositorySession session = createAndBeginSession(); + AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); + + // Create and insert 2 bookmarks, 2nd one is evil (attempts injection). + BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); + BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); + bmk2.guid = "' or '1'='1"; + + db.insert(bmk1); + db.insert(bmk2); + + // Note size of table before delete. + Cursor cur = getAllBookmarks(); + int numBookmarks = cur.getCount(); + + db.purgeGuid(bmk2.guid); + + // Note size of table after delete. + cur = getAllBookmarks(); + int numBookmarksAfterDelete = cur.getCount(); + + // Ensure size of table after delete is *only* 1 less. + assertEquals(numBookmarksAfterDelete, numBookmarks - 1); + + try { + cur.moveToFirst(); + while (!cur.isAfterLast()) { + String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); + if (guid.equals(bmk2.guid)) { + fail("Guid was not deleted!"); + } + cur.moveToNext(); + } + } finally { + cur.close(); + } + dispose(session); + } + + /** + * Verify that data accessor's bulkInsert actually inserts. + * @throws NullCursorException + */ + public void testBulkInsert() throws NullCursorException { + RepositorySession session = createAndBeginSession(); + AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); + + // Have to set androidID of parent manually. + Cursor cur = db.fetch(new String[] { "mobile" } ); + assertEquals(1, cur.getCount()); + cur.moveToFirst(); + int mobileAndroidID = RepoUtils.getIntFromCursor(cur, BrowserContract.Bookmarks._ID); + + BookmarkRecord bookmark1 = BookmarkHelpers.createBookmarkInMobileFolder1(); + BookmarkRecord bookmark2 = BookmarkHelpers.createBookmarkInMobileFolder2(); + bookmark1.androidParentID = mobileAndroidID; + bookmark2.androidParentID = mobileAndroidID; + ArrayList<Record> recordList = new ArrayList<Record>(); + recordList.add(bookmark1); + recordList.add(bookmark2); + db.bulkInsert(recordList); + + String[] guids = new String[] { bookmark1.guid, bookmark2.guid }; + Record[] expected = new Record[] { bookmark1, bookmark2 }; + performWait(fetchRunnable(session, guids, expected)); + dispose(session); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java new file mode 100644 index 000000000..ffde59575 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java @@ -0,0 +1,450 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import java.util.ArrayList; + +import org.json.simple.JSONObject; +import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate; +import org.mozilla.gecko.background.sync.helpers.HistoryHelpers; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.sync.Utils; +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.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 TestAndroidBrowserHistoryRepository extends AndroidBrowserRepositoryTestCase { + + @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 + 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(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java new file mode 100644 index 000000000..783aea1ff --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java @@ -0,0 +1,1063 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; + +import org.json.simple.JSONArray; +import org.mozilla.gecko.R; +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.background.sync.helpers.BookmarkHelpers; +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessBeginDelegate; +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessCreationDelegate; +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFetchDelegate; +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFinishDelegate; +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessStoreDelegate; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserContract.Bookmarks; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.repositories.InactiveSessionException; +import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; +import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; +import org.mozilla.gecko.sync.repositories.NullCursorException; +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; +import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksDataAccessor; +import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepository; +import org.mozilla.gecko.sync.repositories.android.AndroidBrowserBookmarksRepositorySession; +import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; +import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +public class TestBookmarks extends AndroidSyncTestCase { + + protected static final String LOG_TAG = "BookmarksTest"; + + /** + * Trivial test that forbidden records such as pinned items + * will be ignored if processed. + */ + public void testForbiddenItemsAreIgnored() { + final AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository(); + final long now = System.currentTimeMillis(); + final String bookmarksCollection = "bookmarks"; + + final BookmarkRecord pinned = new BookmarkRecord("pinpinpinpin", "bookmarks", now - 1, false); + final BookmarkRecord normal = new BookmarkRecord("baaaaaaaaaaa", "bookmarks", now - 2, false); + + final BookmarkRecord pinnedItems = new BookmarkRecord(Bookmarks.PINNED_FOLDER_GUID, + bookmarksCollection, now - 4, false); + + normal.type = "bookmark"; + pinned.type = "bookmark"; + pinnedItems.type = "folder"; + + pinned.parentID = Bookmarks.PINNED_FOLDER_GUID; + normal.parentID = Bookmarks.TOOLBAR_FOLDER_GUID; + + pinnedItems.parentID = Bookmarks.PLACES_FOLDER_GUID; + + inBegunSession(repo, new SimpleSuccessBeginDelegate() { + @Override + public void onBeginSucceeded(RepositorySession session) { + assertTrue(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(pinned)); + assertTrue(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(pinnedItems)); + assertFalse(((AndroidBrowserBookmarksRepositorySession) session).shouldIgnore(normal)); + finishAndNotify(session); + } + }); + } + + /** + * Trivial test that pinned items will be skipped if present in the DB. + */ + public void testPinnedItemsAreNotRetrieved() { + final AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository(); + + // Ensure that they exist. + setUpFennecPinnedItemsRecord(); + + // They're there in the DB… + final ArrayList<String> roots = fetchChildrenDirect(Bookmarks.FIXED_ROOT_ID); + Logger.info(LOG_TAG, "Roots: " + roots); + assertTrue(roots.contains(Bookmarks.PINNED_FOLDER_GUID)); + + final ArrayList<String> pinned = fetchChildrenDirect(Bookmarks.FIXED_PINNED_LIST_ID); + Logger.info(LOG_TAG, "Pinned: " + pinned); + assertTrue(pinned.contains("dapinneditem")); + + // … but not when we fetch. + final ArrayList<String> guids = fetchGUIDs(repo); + assertFalse(guids.contains(Bookmarks.PINNED_FOLDER_GUID)); + assertFalse(guids.contains("dapinneditem")); + } + + public void testRetrieveFolderHasAccurateChildren() { + AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository(); + + final long now = System.currentTimeMillis(); + + final String folderGUID = "eaaaaaaaafff"; + BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now - 5, false); + BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now - 1, false); + BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now - 3, false); + BookmarkRecord bookmarkC = new BookmarkRecord("aaaaaaaaaccc", "bookmarks", now - 2, false); + + folder.children = childrenFromRecords(bookmarkA, bookmarkB, bookmarkC); + folder.sortIndex = 150; + folder.title = "Test items"; + folder.parentID = "toolbar"; + folder.parentName = "Bookmarks Toolbar"; + folder.type = "folder"; + + bookmarkA.parentID = folderGUID; + bookmarkA.bookmarkURI = "http://example.com/A"; + bookmarkA.title = "Title A"; + bookmarkA.type = "bookmark"; + + bookmarkB.parentID = folderGUID; + bookmarkB.bookmarkURI = "http://example.com/B"; + bookmarkB.title = "Title B"; + bookmarkB.type = "bookmark"; + + bookmarkC.parentID = folderGUID; + bookmarkC.bookmarkURI = "http://example.com/C"; + bookmarkC.title = "Title C"; + bookmarkC.type = "bookmark"; + + BookmarkRecord[] folderOnly = new BookmarkRecord[1]; + BookmarkRecord[] children = new BookmarkRecord[3]; + + folderOnly[0] = folder; + + children[0] = bookmarkA; + children[1] = bookmarkB; + children[2] = bookmarkC; + + wipe(); + Logger.debug(getName(), "Storing just folder..."); + storeRecordsInSession(repo, folderOnly, null); + + // We don't have any children, despite our insistence upon storing. + assertChildrenAreOrdered(repo, folderGUID, new Record[] {}); + + // Now store the children. + Logger.debug(getName(), "Storing children..."); + storeRecordsInSession(repo, children, null); + + // Now we have children, but their order is not determined, because + // they were stored out-of-session with the original folder. + assertChildrenAreUnordered(repo, folderGUID, children); + + // Now if we store the folder record again, they'll be put in the + // right place. + folder.lastModified++; + Logger.debug(getName(), "Storing just folder again..."); + storeRecordsInSession(repo, folderOnly, null); + Logger.debug(getName(), "Fetching children yet again..."); + assertChildrenAreOrdered(repo, folderGUID, children); + + // Now let's see what happens when we see records in the same session. + BookmarkRecord[] parentMixed = new BookmarkRecord[4]; + BookmarkRecord[] parentFirst = new BookmarkRecord[4]; + BookmarkRecord[] parentLast = new BookmarkRecord[4]; + + // None of our records have a position set. + assertTrue(bookmarkA.androidPosition <= 0); + assertTrue(bookmarkB.androidPosition <= 0); + assertTrue(bookmarkC.androidPosition <= 0); + + parentMixed[1] = folder; + parentMixed[0] = bookmarkA; + parentMixed[2] = bookmarkC; + parentMixed[3] = bookmarkB; + + parentFirst[0] = folder; + parentFirst[1] = bookmarkC; + parentFirst[2] = bookmarkA; + parentFirst[3] = bookmarkB; + + parentLast[3] = folder; + parentLast[0] = bookmarkB; + parentLast[1] = bookmarkA; + parentLast[2] = bookmarkC; + + wipe(); + storeRecordsInSession(repo, parentMixed, null); + assertChildrenAreOrdered(repo, folderGUID, children); + + wipe(); + storeRecordsInSession(repo, parentFirst, null); + assertChildrenAreOrdered(repo, folderGUID, children); + + wipe(); + storeRecordsInSession(repo, parentLast, null); + assertChildrenAreOrdered(repo, folderGUID, children); + + // Ensure that records are ordered even if we re-process the folder. + wipe(); + storeRecordsInSession(repo, parentLast, null); + folder.lastModified++; + storeRecordsInSession(repo, folderOnly, null); + assertChildrenAreOrdered(repo, folderGUID, children); + } + + public void testMergeFoldersPreservesSaneOrder() { + AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository(); + + final long now = System.currentTimeMillis(); + final String folderGUID = "mobile"; + + wipe(); + final long mobile = setUpFennecMobileRecord(); + + // No children. + assertChildrenAreUnordered(repo, folderGUID, new Record[] {}); + + // Add some, as Fennec would. + fennecAddBookmark("Bookmark One", "http://example.com/fennec/One"); + fennecAddBookmark("Bookmark Two", "http://example.com/fennec/Two"); + + Logger.debug(getName(), "Fetching children..."); + JSONArray folderChildren = fetchChildrenForGUID(repo, folderGUID); + + assertTrue(folderChildren != null); + Logger.debug(getName(), "Children are " + folderChildren.toJSONString()); + assertEquals(2, folderChildren.size()); + String guidOne = (String) folderChildren.get(0); + String guidTwo = (String) folderChildren.get(1); + + // Make sure positions were saved. + assertChildrenAreDirect(mobile, new String[] { + guidOne, + guidTwo + }); + + // Add some through Sync. + BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now, false); + BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now, false); + BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now, false); + + folder.children = childrenFromRecords(bookmarkA, bookmarkB); + folder.sortIndex = 150; + folder.title = "Mobile Bookmarks"; + folder.parentID = "places"; + folder.parentName = ""; + folder.type = "folder"; + + bookmarkA.parentID = folderGUID; + bookmarkA.parentName = "Mobile Bookmarks"; // Using this title exercises Bug 748898. + bookmarkA.bookmarkURI = "http://example.com/A"; + bookmarkA.title = "Title A"; + bookmarkA.type = "bookmark"; + + bookmarkB.parentID = folderGUID; + bookmarkB.parentName = "mobile"; + bookmarkB.bookmarkURI = "http://example.com/B"; + bookmarkB.title = "Title B"; + bookmarkB.type = "bookmark"; + + BookmarkRecord[] parentMixed = new BookmarkRecord[3]; + parentMixed[0] = bookmarkA; + parentMixed[1] = folder; + parentMixed[2] = bookmarkB; + + storeRecordsInSession(repo, parentMixed, null); + + BookmarkRecord expectedOne = new BookmarkRecord(guidOne, "bookmarks", now - 10, false); + BookmarkRecord expectedTwo = new BookmarkRecord(guidTwo, "bookmarks", now - 10, false); + + // We want the server to win in this case, and otherwise to preserve order. + // TODO + assertChildrenAreOrdered(repo, folderGUID, new Record[] { + bookmarkA, + bookmarkB, + expectedOne, + expectedTwo + }); + + // Furthermore, the children of that folder should be correct in the DB. + ContentResolver cr = getApplicationContext().getContentResolver(); + final long folderId = fennecGetFolderId(cr, folderGUID); + Logger.debug(getName(), "Folder " + folderGUID + " => " + folderId); + + assertChildrenAreDirect(folderId, new String[] { + bookmarkA.guid, + bookmarkB.guid, + expectedOne.guid, + expectedTwo.guid + }); + } + + /** + * Apply a folder record whose children array is already accurately + * stored in the database. Verify that the parent folder is not flagged + * for reupload (i.e., that its modified time is *ahem* unmodified). + */ + public void testNoReorderingMeansNoReupload() { + AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository(); + + final long now = System.currentTimeMillis(); + + final String folderGUID = "eaaaaaaaafff"; + BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now -5, false); + BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now -1, false); + BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now -3, false); + + folder.children = childrenFromRecords(bookmarkA, bookmarkB); + folder.sortIndex = 150; + folder.title = "Test items"; + folder.parentID = "toolbar"; + folder.parentName = "Bookmarks Toolbar"; + folder.type = "folder"; + + bookmarkA.parentID = folderGUID; + bookmarkA.bookmarkURI = "http://example.com/A"; + bookmarkA.title = "Title A"; + bookmarkA.type = "bookmark"; + + bookmarkB.parentID = folderGUID; + bookmarkB.bookmarkURI = "http://example.com/B"; + bookmarkB.title = "Title B"; + bookmarkB.type = "bookmark"; + + BookmarkRecord[] abf = new BookmarkRecord[3]; + BookmarkRecord[] justFolder = new BookmarkRecord[1]; + + abf[0] = bookmarkA; + abf[1] = bookmarkB; + abf[2] = folder; + + justFolder[0] = folder; + + final String[] abGUIDs = new String[] { bookmarkA.guid, bookmarkB.guid }; + final Record[] abRecords = new Record[] { bookmarkA, bookmarkB }; + final String[] baGUIDs = new String[] { bookmarkB.guid, bookmarkA.guid }; + final Record[] baRecords = new Record[] { bookmarkB, bookmarkA }; + + wipe(); + Logger.debug(getName(), "Storing A, B, folder..."); + storeRecordsInSession(repo, abf, null); + + ContentResolver cr = getApplicationContext().getContentResolver(); + final long folderID = fennecGetFolderId(cr, folderGUID); + assertChildrenAreOrdered(repo, folderGUID, abRecords); + assertChildrenAreDirect(folderID, abGUIDs); + + // To ensure an interval. + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + + // Store the same folder record again, and check the tracking. + // Because the folder array didn't change, + // the item is still tracked to not be uploaded. + folder.lastModified = System.currentTimeMillis() + 1; + HashSet<String> tracked = new HashSet<String>(); + storeRecordsInSession(repo, justFolder, tracked); + assertChildrenAreOrdered(repo, folderGUID, abRecords); + assertChildrenAreDirect(folderID, abGUIDs); + + assertTrue(tracked.contains(folderGUID)); + + // Store again, but with a different order. + tracked = new HashSet<String>(); + folder.children = childrenFromRecords(bookmarkB, bookmarkA); + folder.lastModified = System.currentTimeMillis() + 1; + storeRecordsInSession(repo, justFolder, tracked); + assertChildrenAreOrdered(repo, folderGUID, baRecords); + assertChildrenAreDirect(folderID, baGUIDs); + + // Now it's going to be reuploaded. + assertFalse(tracked.contains(folderGUID)); + } + + /** + * Exercise the deletion of folders when their children have not been + * marked as deleted. In a database with constraints, this would fail + * if we simply deleted the records, so we move them first. + */ + public void testFolderDeletionOrphansChildren() { + AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository(); + + long now = System.currentTimeMillis(); + + // Add a folder and four children. + final String folderGUID = "eaaaaaaaafff"; + BookmarkRecord folder = new BookmarkRecord(folderGUID, "bookmarks", now -5, false); + BookmarkRecord bookmarkA = new BookmarkRecord("daaaaaaaaaaa", "bookmarks", now -1, false); + BookmarkRecord bookmarkB = new BookmarkRecord("baaaaaaaabbb", "bookmarks", now -3, false); + BookmarkRecord bookmarkC = new BookmarkRecord("daaaaaaaaccc", "bookmarks", now -7, false); + BookmarkRecord bookmarkD = new BookmarkRecord("baaaaaaaaddd", "bookmarks", now -4, false); + + folder.children = childrenFromRecords(bookmarkA, bookmarkB, bookmarkC, bookmarkD); + folder.sortIndex = 150; + folder.title = "Test items"; + folder.parentID = "toolbar"; + folder.parentName = "Bookmarks Toolbar"; + folder.type = "folder"; + + bookmarkA.parentID = folderGUID; + bookmarkA.bookmarkURI = "http://example.com/A"; + bookmarkA.title = "Title A"; + bookmarkA.type = "bookmark"; + + bookmarkB.parentID = folderGUID; + bookmarkB.bookmarkURI = "http://example.com/B"; + bookmarkB.title = "Title B"; + bookmarkB.type = "bookmark"; + + bookmarkC.parentID = folderGUID; + bookmarkC.bookmarkURI = "http://example.com/C"; + bookmarkC.title = "Title C"; + bookmarkC.type = "bookmark"; + + bookmarkD.parentID = folderGUID; + bookmarkD.bookmarkURI = "http://example.com/D"; + bookmarkD.title = "Title D"; + bookmarkD.type = "bookmark"; + + BookmarkRecord[] abfcd = new BookmarkRecord[5]; + BookmarkRecord[] justFolder = new BookmarkRecord[1]; + abfcd[0] = bookmarkA; + abfcd[1] = bookmarkB; + abfcd[2] = folder; + abfcd[3] = bookmarkC; + abfcd[4] = bookmarkD; + + justFolder[0] = folder; + + final String[] abcdGUIDs = new String[] { bookmarkA.guid, bookmarkB.guid, bookmarkC.guid, bookmarkD.guid }; + final Record[] abcdRecords = new Record[] { bookmarkA, bookmarkB, bookmarkC, bookmarkD }; + + wipe(); + Logger.debug(getName(), "Storing A, B, folder, C, D..."); + storeRecordsInSession(repo, abfcd, null); + + // Verify that it worked. + ContentResolver cr = getApplicationContext().getContentResolver(); + final long folderID = fennecGetFolderId(cr, folderGUID); + assertChildrenAreOrdered(repo, folderGUID, abcdRecords); + assertChildrenAreDirect(folderID, abcdGUIDs); + + now = System.currentTimeMillis(); + + // Add one child to unsorted bookmarks. + BookmarkRecord unsortedA = new BookmarkRecord("yiamunsorted", "bookmarks", now, false); + unsortedA.parentID = "unfiled"; + unsortedA.title = "Unsorted A"; + unsortedA.type = "bookmark"; + unsortedA.androidPosition = 0; + + BookmarkRecord[] ua = new BookmarkRecord[1]; + ua[0] = unsortedA; + + storeRecordsInSession(repo, ua, null); + + // Ensure that the database is in this state. + assertChildrenAreOrdered(repo, "unfiled", ua); + + // Delete the second child, the folder, and then the third child. + bookmarkB.bookmarkURI = bookmarkC.bookmarkURI = folder.bookmarkURI = null; + bookmarkB.deleted = bookmarkC.deleted = folder.deleted = true; + bookmarkB.title = bookmarkC.title = folder.title = null; + + // Nulling the type of folder is very important: it verifies + // that the session can behave correctly according to local type. + bookmarkB.type = bookmarkC.type = folder.type = null; + + bookmarkB.lastModified = bookmarkC.lastModified = folder.lastModified = now = System.currentTimeMillis(); + + BookmarkRecord[] deletions = new BookmarkRecord[] { bookmarkB, folder, bookmarkC }; + storeRecordsInSession(repo, deletions, null); + + // Verify that the unsorted bookmarks folder contains its child and the + // first and fourth children of the now-deleted folder. + // Also verify that the folder is gone. + long unsortedID = fennecGetFolderId(cr, "unfiled"); + long toolbarID = fennecGetFolderId(cr, "toolbar"); + String[] expected = new String[] { unsortedA.guid, bookmarkA.guid, bookmarkD.guid }; + + // This will trigger positioning. + assertChildrenAreUnordered(repo, "unfiled", new Record[] { unsortedA, bookmarkA, bookmarkD }); + assertChildrenAreDirect(unsortedID, expected); + assertChildrenAreDirect(toolbarID, new String[] {}); + } + + /** + * A test where we expect to replace a local folder with a new folder (with a + * new GUID), whilst adding children to it. Verifies that replace and insert + * co-operate. + */ + public void testInsertAndReplaceGuid() { + AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository(); + wipe(); + + BookmarkRecord folder1 = BookmarkHelpers.createFolder1(); + BookmarkRecord folder2 = BookmarkHelpers.createFolder2(); // child of folder1 + BookmarkRecord folder3 = BookmarkHelpers.createFolder3(); // child of folder2 + BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); // child of folder1 + BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); // child of folder1 + BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3(); // child of folder2 + BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4(); // child of folder3 + + BookmarkRecord[] records = new BookmarkRecord[] { + folder1, folder2, folder3, + bmk1, bmk4 + }; + storeRecordsInSession(repo, records, null); + + assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, folder2 }); + assertChildrenAreUnordered(repo, folder2.guid, new Record[] { folder3 }); + assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 }); + + // Replace folder3 with a record with a new GUID, and add bmk4 as folder3's child. + final long now = System.currentTimeMillis(); + folder3.guid = Utils.generateGuid(); + folder3.lastModified = now; + bmk4.title = bmk4.title + "/NEW"; + bmk4.parentID = folder3.guid; // Incoming child knows its parent. + bmk4.parentName = folder3.title; + bmk4.lastModified = now; + + // Order of store should not matter. + ArrayList<BookmarkRecord> changedRecords = new ArrayList<BookmarkRecord>(); + changedRecords.add(bmk2); changedRecords.add(bmk3); changedRecords.add(bmk4); changedRecords.add(folder3); + Collections.shuffle(changedRecords); + storeRecordsInSession(repo, changedRecords.toArray(new BookmarkRecord[changedRecords.size()]), null); + + assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, bmk2, folder2 }); + assertChildrenAreUnordered(repo, folder2.guid, new Record[] { bmk3, folder3 }); + assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 }); + + assertNotNull(fetchGUID(repo, folder3.guid)); + assertEquals(bmk4.title, fetchGUID(repo, bmk4.guid).title); + } + + /** + * A test where we expect to replace a local folder with a new folder (with a + * new title but the same GUID), whilst adding children to it. Verifies that + * replace and insert co-operate. + */ + public void testInsertAndReplaceTitle() { + AndroidBrowserBookmarksRepository repo = new AndroidBrowserBookmarksRepository(); + wipe(); + + BookmarkRecord folder1 = BookmarkHelpers.createFolder1(); + BookmarkRecord folder2 = BookmarkHelpers.createFolder2(); // child of folder1 + BookmarkRecord folder3 = BookmarkHelpers.createFolder3(); // child of folder2 + BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); // child of folder1 + BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); // child of folder1 + BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3(); // child of folder2 + BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4(); // child of folder3 + + BookmarkRecord[] records = new BookmarkRecord[] { + folder1, folder2, folder3, + bmk1, bmk4 + }; + storeRecordsInSession(repo, records, null); + + assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, folder2 }); + assertChildrenAreUnordered(repo, folder2.guid, new Record[] { folder3 }); + assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 }); + + // Rename folder1, and add bmk2 as folder1's child. + final long now = System.currentTimeMillis(); + folder1.title = folder1.title + "/NEW"; + folder1.lastModified = now; + bmk2.title = bmk2.title + "/NEW"; + bmk2.parentID = folder1.guid; // Incoming child knows its parent. + bmk2.parentName = folder1.title; + bmk2.lastModified = now; + + // Order of store should not matter. + ArrayList<BookmarkRecord> changedRecords = new ArrayList<BookmarkRecord>(); + changedRecords.add(bmk2); changedRecords.add(bmk3); changedRecords.add(folder1); + Collections.shuffle(changedRecords); + storeRecordsInSession(repo, changedRecords.toArray(new BookmarkRecord[changedRecords.size()]), null); + + assertChildrenAreUnordered(repo, folder1.guid, new Record[] { bmk1, bmk2, folder2 }); + assertChildrenAreUnordered(repo, folder2.guid, new Record[] { bmk3, folder3 }); + assertChildrenAreUnordered(repo, folder3.guid, new Record[] { bmk4 }); + + assertEquals(folder1.title, fetchGUID(repo, folder1.guid).title); + assertEquals(bmk2.title, fetchGUID(repo, bmk2.guid).title); + } + + /** + * Create and begin a new session, handing control to the delegate when started. + * Returns when the delegate has notified. + */ + public void inBegunSession(final AndroidBrowserBookmarksRepository repo, + final RepositorySessionBeginDelegate beginDelegate) { + Runnable go = new Runnable() { + @Override + public void run() { + RepositorySessionCreationDelegate delegate = new SimpleSuccessCreationDelegate() { + @Override + public void onSessionCreated(final RepositorySession session) { + try { + session.begin(beginDelegate); + } catch (InvalidSessionTransitionException e) { + performNotify(e); + } + } + }; + repo.createSession(delegate, getApplicationContext()); + } + }; + performWait(go); + } + + /** + * Finish the provided session, notifying on success. + * + * @param session + */ + public void finishAndNotify(final RepositorySession session) { + try { + session.finish(new SimpleSuccessFinishDelegate() { + @Override + public void onFinishSucceeded(RepositorySession session, + RepositorySessionBundle bundle) { + performNotify(); + } + }); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + + /** + * Simple helper class for fetching all records. + * The fetched records' GUIDs are stored in `fetchedGUIDs`. + */ + public class SimpleFetchAllBeginDelegate extends SimpleSuccessBeginDelegate { + public final ArrayList<String> fetchedGUIDs = new ArrayList<String>(); + + @Override + public void onBeginSucceeded(final RepositorySession session) { + RepositorySessionFetchRecordsDelegate fetchDelegate = new SimpleSuccessFetchDelegate() { + + @Override + public void onFetchedRecord(Record record) { + fetchedGUIDs.add(record.guid); + } + + @Override + public void onFetchCompleted(long end) { + finishAndNotify(session); + } + }; + session.fetchSince(0, fetchDelegate); + } + } + + /** + * Simple helper class for fetching a single record by GUID. + * The fetched record is stored in `fetchedRecord`. + */ + public class SimpleFetchOneBeginDelegate extends SimpleSuccessBeginDelegate { + public final String guid; + public Record fetchedRecord = null; + + public SimpleFetchOneBeginDelegate(String guid) { + this.guid = guid; + } + + @Override + public void onBeginSucceeded(final RepositorySession session) { + RepositorySessionFetchRecordsDelegate fetchDelegate = new SimpleSuccessFetchDelegate() { + + @Override + public void onFetchedRecord(Record record) { + fetchedRecord = record; + } + + @Override + public void onFetchCompleted(long end) { + finishAndNotify(session); + } + }; + try { + session.fetch(new String[] { guid }, fetchDelegate); + } catch (InactiveSessionException e) { + performNotify("Session is inactive.", e); + } + } + } + + /** + * Create a new session for the given repository, storing each record + * from the provided array. Notifies on failure or success. + * + * Optionally populates a provided Collection with tracked items. + * @param repo + * @param records + * @param tracked + */ + public void storeRecordsInSession(AndroidBrowserBookmarksRepository repo, + final BookmarkRecord[] records, + final Collection<String> tracked) { + SimpleSuccessBeginDelegate beginDelegate = new SimpleSuccessBeginDelegate() { + @Override + public void onBeginSucceeded(final RepositorySession session) { + RepositorySessionStoreDelegate storeDelegate = new SimpleSuccessStoreDelegate() { + + @Override + public void onStoreCompleted(final long storeEnd) { + // Pass back whatever we tracked. + if (tracked != null) { + Iterator<String> iter = session.getTrackedRecordIDs(); + while (iter.hasNext()) { + tracked.add(iter.next()); + } + } + finishAndNotify(session); + } + + @Override + public void onRecordStoreSucceeded(String guid) { + } + }; + session.setStoreDelegate(storeDelegate); + for (BookmarkRecord record : records) { + try { + session.store(record); + } catch (NoStoreDelegateException e) { + // Never happens. + } + } + session.storeDone(); + } + }; + inBegunSession(repo, beginDelegate); + } + + public ArrayList<String> fetchGUIDs(AndroidBrowserBookmarksRepository repo) { + SimpleFetchAllBeginDelegate beginDelegate = new SimpleFetchAllBeginDelegate(); + inBegunSession(repo, beginDelegate); + return beginDelegate.fetchedGUIDs; + } + + public BookmarkRecord fetchGUID(AndroidBrowserBookmarksRepository repo, + final String guid) { + Logger.info(LOG_TAG, "Fetching for " + guid); + SimpleFetchOneBeginDelegate beginDelegate = new SimpleFetchOneBeginDelegate(guid); + inBegunSession(repo, beginDelegate); + Logger.info(LOG_TAG, "Fetched " + beginDelegate.fetchedRecord); + assertTrue(beginDelegate.fetchedRecord != null); + return (BookmarkRecord) beginDelegate.fetchedRecord; + } + + public JSONArray fetchChildrenForGUID(AndroidBrowserBookmarksRepository repo, + final String guid) { + return fetchGUID(repo, guid).children; + } + + @SuppressWarnings("unchecked") + protected static JSONArray childrenFromRecords(BookmarkRecord... records) { + JSONArray children = new JSONArray(); + for (BookmarkRecord record : records) { + children.add(record.guid); + } + return children; + } + + + protected void updateRow(ContentValues values) { + Uri uri = BrowserContractHelpers.BOOKMARKS_CONTENT_URI; + final String where = BrowserContract.Bookmarks.GUID + " = ?"; + final String[] args = new String[] { values.getAsString(BrowserContract.Bookmarks.GUID) }; + getApplicationContext().getContentResolver().update(uri, values, where, args); + } + + protected Uri insertRow(ContentValues values) { + Uri uri = BrowserContractHelpers.BOOKMARKS_CONTENT_URI; + return getApplicationContext().getContentResolver().insert(uri, values); + } + + protected static ContentValues specialFolder() { + ContentValues values = new ContentValues(); + + final long now = System.currentTimeMillis(); + values.put(Bookmarks.DATE_CREATED, now); + values.put(Bookmarks.DATE_MODIFIED, now); + values.put(Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_FOLDER); + + return values; + } + + protected static ContentValues fennecMobileRecordWithoutTitle() { + ContentValues values = specialFolder(); + values.put(BrowserContract.SyncColumns.GUID, "mobile"); + values.putNull(BrowserContract.Bookmarks.TITLE); + + return values; + } + + protected ContentValues fennecPinnedItemsRecord() { + final ContentValues values = specialFolder(); + final String title = getApplicationContext().getResources().getString(R.string.bookmarks_folder_pinned); + + values.put(BrowserContract.SyncColumns.GUID, Bookmarks.PINNED_FOLDER_GUID); + values.put(Bookmarks._ID, Bookmarks.FIXED_PINNED_LIST_ID); + values.put(Bookmarks.PARENT, Bookmarks.FIXED_ROOT_ID); + values.put(Bookmarks.TITLE, title); + return values; + } + + protected static ContentValues fennecPinnedChildItemRecord() { + ContentValues values = new ContentValues(); + + final long now = System.currentTimeMillis(); + + values.put(BrowserContract.SyncColumns.GUID, "dapinneditem"); + values.put(Bookmarks.DATE_CREATED, now); + values.put(Bookmarks.DATE_MODIFIED, now); + values.put(Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_BOOKMARK); + values.put(Bookmarks.URL, "user-entered:foobar"); + values.put(Bookmarks.PARENT, Bookmarks.FIXED_PINNED_LIST_ID); + values.put(Bookmarks.TITLE, "Foobar"); + return values; + } + + protected long setUpFennecMobileRecordWithoutTitle() { + ContentResolver cr = getApplicationContext().getContentResolver(); + ContentValues values = fennecMobileRecordWithoutTitle(); + updateRow(values); + return fennecGetMobileBookmarksFolderId(cr); + } + + protected long setUpFennecMobileRecord() { + ContentResolver cr = getApplicationContext().getContentResolver(); + ContentValues values = fennecMobileRecordWithoutTitle(); + values.put(BrowserContract.Bookmarks.PARENT, BrowserContract.Bookmarks.FIXED_ROOT_ID); + String title = getApplicationContext().getResources().getString(R.string.bookmarks_folder_mobile); + values.put(BrowserContract.Bookmarks.TITLE, title); + updateRow(values); + return fennecGetMobileBookmarksFolderId(cr); + } + + protected void setUpFennecPinnedItemsRecord() { + insertRow(fennecPinnedItemsRecord()); + insertRow(fennecPinnedChildItemRecord()); + } + + // + // Fennec fake layer. + // + private Uri appendProfile(Uri uri) { + final String defaultProfile = "default"; // Fennec constant removed in Bug 715307. + return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, defaultProfile).build(); + } + + private long fennecGetFolderId(ContentResolver cr, String guid) { + Cursor c = null; + try { + c = cr.query(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI), + new String[] { BrowserContract.Bookmarks._ID }, + BrowserContract.Bookmarks.GUID + " = ?", + new String[] { guid }, + null); + + if (c.moveToFirst()) { + return c.getLong(c.getColumnIndexOrThrow(BrowserContract.Bookmarks._ID)); + } + return -1; + } finally { + if (c != null) { + c.close(); + } + } + } + + private long fennecGetMobileBookmarksFolderId(ContentResolver cr) { + return fennecGetFolderId(cr, BrowserContract.Bookmarks.MOBILE_FOLDER_GUID); + } + + public void fennecAddBookmark(String title, String uri) { + ContentResolver cr = getApplicationContext().getContentResolver(); + + long folderId = fennecGetMobileBookmarksFolderId(cr); + if (folderId < 0) { + return; + } + + ContentValues values = new ContentValues(); + values.put(BrowserContract.Bookmarks.TITLE, title); + values.put(BrowserContract.Bookmarks.URL, uri); + values.put(BrowserContract.Bookmarks.PARENT, folderId); + + // Restore deleted record if possible + values.put(BrowserContract.Bookmarks.IS_DELETED, 0); + + Logger.debug(getName(), "Adding bookmark " + title + ", " + uri + " in " + folderId); + int updated = cr.update(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI), + values, + BrowserContract.Bookmarks.URL + " = ?", + new String[] { uri }); + + if (updated == 0) { + Uri insert = cr.insert(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI), values); + long idFromUri = ContentUris.parseId(insert); + Logger.debug(getName(), "Inserted " + uri + " as " + idFromUri); + Logger.debug(getName(), "Position is " + getPosition(idFromUri)); + } + } + + private long getPosition(long idFromUri) { + ContentResolver cr = getApplicationContext().getContentResolver(); + Cursor c = cr.query(appendProfile(BrowserContractHelpers.BOOKMARKS_CONTENT_URI), + new String[] { BrowserContract.Bookmarks.POSITION }, + BrowserContract.Bookmarks._ID + " = ?", + new String[] { String.valueOf(idFromUri) }, + null); + if (!c.moveToFirst()) { + return -2; + } + return c.getLong(0); + } + + protected AndroidBrowserBookmarksDataAccessor dataAccessor = null; + protected AndroidBrowserBookmarksDataAccessor getDataAccessor() { + if (dataAccessor == null) { + dataAccessor = new AndroidBrowserBookmarksDataAccessor(getApplicationContext()); + } + return dataAccessor; + } + + protected void wipe() { + Logger.debug(getName(), "Wiping."); + getDataAccessor().wipe(); + } + + protected void assertChildrenAreOrdered(AndroidBrowserBookmarksRepository repo, String guid, Record[] expected) { + Logger.debug(getName(), "Fetching children..."); + JSONArray folderChildren = fetchChildrenForGUID(repo, guid); + + assertTrue(folderChildren != null); + Logger.debug(getName(), "Children are " + folderChildren.toJSONString()); + assertEquals(expected.length, folderChildren.size()); + for (int i = 0; i < expected.length; ++i) { + assertEquals(expected[i].guid, ((String) folderChildren.get(i))); + } + } + + protected void assertChildrenAreUnordered(AndroidBrowserBookmarksRepository repo, String guid, Record[] expected) { + Logger.debug(getName(), "Fetching children..."); + JSONArray folderChildren = fetchChildrenForGUID(repo, guid); + + assertTrue(folderChildren != null); + Logger.debug(getName(), "Children are " + folderChildren.toJSONString()); + assertEquals(expected.length, folderChildren.size()); + for (Record record : expected) { + folderChildren.contains(record.guid); + } + } + + /** + * Return a sequence of children GUIDs for the provided folder ID. + */ + protected ArrayList<String> fetchChildrenDirect(long id) { + Logger.debug(getName(), "Fetching children directly from DB..."); + final ArrayList<String> out = new ArrayList<String>(); + final AndroidBrowserBookmarksDataAccessor accessor = new AndroidBrowserBookmarksDataAccessor(getApplicationContext()); + Cursor cur = null; + try { + cur = accessor.getChildren(id); + } catch (NullCursorException e) { + fail("Got null cursor."); + } + try { + if (!cur.moveToFirst()) { + return out; + } + final int guidCol = cur.getColumnIndex(BrowserContract.SyncColumns.GUID); + while (!cur.isAfterLast()) { + out.add(cur.getString(guidCol)); + cur.moveToNext(); + } + } finally { + cur.close(); + } + return out; + } + + /** + * Assert that the children of the provided ID are correct and positioned in the database. + * @param id + * @param guids + */ + protected void assertChildrenAreDirect(long id, String[] guids) { + Logger.debug(getName(), "Fetching children directly from DB..."); + AndroidBrowserBookmarksDataAccessor accessor = new AndroidBrowserBookmarksDataAccessor(getApplicationContext()); + Cursor cur = null; + try { + cur = accessor.getChildren(id); + } catch (NullCursorException e) { + fail("Got null cursor."); + } + try { + if (guids == null || guids.length == 0) { + assertFalse(cur.moveToFirst()); + return; + } + + assertTrue(cur.moveToFirst()); + int i = 0; + final int guidCol = cur.getColumnIndex(BrowserContract.SyncColumns.GUID); + final int posCol = cur.getColumnIndex(BrowserContract.Bookmarks.POSITION); + while (!cur.isAfterLast()) { + assertTrue(i < guids.length); + final String guid = cur.getString(guidCol); + final int pos = cur.getInt(posCol); + Logger.debug(getName(), "Fetched child: " + guid + " has position " + pos); + assertEquals(guids[i], guid); + assertEquals(i, pos); + + ++i; + cur.moveToNext(); + } + assertEquals(guids.length, i); + } finally { + cur.close(); + } + } +} + +/** +TODO + +Test for storing a record that will reconcile to mobile; postcondition is +that there's still a directory called mobile that includes all the items that +it used to. + +mobile folder created without title. +Unsorted put in mobile??? +Tests for children retrieval +Tests for children merge +Tests for modify retrieve parent when child added, removed, reordered (oh, reorder is hard! Any change, then.) +Safety mode? +Test storing folder first, contents first. +Store folder in next session. Verify order recovery. + + +*/ diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabase.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabase.java new file mode 100644 index 000000000..198073fcf --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabase.java @@ -0,0 +1,200 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import java.util.ArrayList; + +import org.json.simple.JSONArray; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.repositories.NullCursorException; +import org.mozilla.gecko.sync.repositories.android.ClientsDatabase; +import org.mozilla.gecko.sync.repositories.android.RepoUtils; +import org.mozilla.gecko.sync.repositories.domain.ClientRecord; +import org.mozilla.gecko.sync.setup.Constants; + +import android.database.Cursor; +import android.test.AndroidTestCase; + +public class TestClientsDatabase extends AndroidTestCase { + + protected ClientsDatabase db; + + public void setUp() { + db = new ClientsDatabase(mContext); + db.wipeDB(); + } + + public void testStoreAndFetch() { + ClientRecord record = new ClientRecord(); + String profileConst = Constants.DEFAULT_PROFILE; + db.store(profileConst, record); + + Cursor cur = null; + try { + // Test stored item gets fetched correctly. + cur = db.fetchClientsCursor(record.guid, profileConst); + assertTrue(cur.moveToFirst()); + assertEquals(1, cur.getCount()); + + String guid = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID); + String profileId = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_PROFILE); + String clientName = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_NAME); + String clientType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_TYPE); + + assertEquals(record.guid, guid); + assertEquals(profileConst, profileId); + assertEquals(record.name, clientName); + assertEquals(record.type, clientType); + } catch (NullCursorException e) { + fail("Should not have NullCursorException"); + } finally { + if (cur != null) { + cur.close(); + } + } + } + + public void testStoreAndFetchSpecificCommands() { + String accountGUID = Utils.generateGuid(); + ArrayList<String> args = new ArrayList<String>(); + args.add("URI of Page"); + args.add("Sender GUID"); + args.add("Title of Page"); + String jsonArgs = JSONArray.toJSONString(args); + + Cursor cur = null; + try { + db.store(accountGUID, "displayURI", jsonArgs); + + // This row should not show up in the fetch. + args.add("Another arg."); + db.store(accountGUID, "displayURI", JSONArray.toJSONString(args)); + + // Test stored item gets fetched correctly. + cur = db.fetchSpecificCommand(accountGUID, "displayURI", jsonArgs); + assertTrue(cur.moveToFirst()); + assertEquals(1, cur.getCount()); + + String guid = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID); + String commandType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_COMMAND); + String fetchedArgs = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ARGS); + + assertEquals(accountGUID, guid); + assertEquals("displayURI", commandType); + assertEquals(jsonArgs, fetchedArgs); + } catch (NullCursorException e) { + fail("Should not have NullCursorException"); + } finally { + if (cur != null) { + cur.close(); + } + } + } + + public void testFetchCommandsForClient() { + String accountGUID = Utils.generateGuid(); + ArrayList<String> args = new ArrayList<String>(); + args.add("URI of Page"); + args.add("Sender GUID"); + args.add("Title of Page"); + String jsonArgs = JSONArray.toJSONString(args); + + Cursor cur = null; + try { + db.store(accountGUID, "displayURI", jsonArgs); + + // This row should ALSO show up in the fetch. + args.add("Another arg."); + db.store(accountGUID, "displayURI", JSONArray.toJSONString(args)); + + // Test both stored items with the same GUID but different command are fetched. + cur = db.fetchCommandsForClient(accountGUID); + assertTrue(cur.moveToFirst()); + assertEquals(2, cur.getCount()); + } catch (NullCursorException e) { + fail("Should not have NullCursorException"); + } finally { + if (cur != null) { + cur.close(); + } + } + } + + @SuppressWarnings("resource") + public void testDelete() { + ClientRecord record1 = new ClientRecord(); + ClientRecord record2 = new ClientRecord(); + String profileConst = Constants.DEFAULT_PROFILE; + + db.store(profileConst, record1); + db.store(profileConst, record2); + + Cursor cur = null; + try { + // Test record doesn't exist after delete. + db.deleteClient(record1.guid, profileConst); + cur = db.fetchClientsCursor(record1.guid, profileConst); + assertFalse(cur.moveToFirst()); + assertEquals(0, cur.getCount()); + + // Test record2 still there after deleting record1. + cur = db.fetchClientsCursor(record2.guid, profileConst); + assertTrue(cur.moveToFirst()); + assertEquals(1, cur.getCount()); + + String guid = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_ACCOUNT_GUID); + String profileId = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_PROFILE); + String clientName = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_NAME); + String clientType = RepoUtils.getStringFromCursor(cur, ClientsDatabase.COL_TYPE); + + assertEquals(record2.guid, guid); + assertEquals(profileConst, profileId); + assertEquals(record2.name, clientName); + assertEquals(record2.type, clientType); + } catch (NullCursorException e) { + fail("Should not have NullCursorException"); + } finally { + if (cur != null) { + cur.close(); + } + } + } + + @SuppressWarnings("resource") + public void testWipe() { + ClientRecord record1 = new ClientRecord(); + ClientRecord record2 = new ClientRecord(); + String profileConst = Constants.DEFAULT_PROFILE; + + db.store(profileConst, record1); + db.store(profileConst, record2); + + + Cursor cur = null; + try { + // Test before wipe the records are there. + cur = db.fetchClientsCursor(record2.guid, profileConst); + assertTrue(cur.moveToFirst()); + assertEquals(1, cur.getCount()); + cur = db.fetchClientsCursor(record2.guid, profileConst); + assertTrue(cur.moveToFirst()); + assertEquals(1, cur.getCount()); + + // Test after wipe neither record exists. + db.wipeClientsTable(); + cur = db.fetchClientsCursor(record2.guid, profileConst); + assertFalse(cur.moveToFirst()); + assertEquals(0, cur.getCount()); + cur = db.fetchClientsCursor(record1.guid, profileConst); + assertFalse(cur.moveToFirst()); + assertEquals(0, cur.getCount()); + } catch (NullCursorException e) { + fail("Should not have NullCursorException"); + } finally { + if (cur != null) { + cur.close(); + } + } + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java new file mode 100644 index 000000000..65b14e860 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.mozilla.gecko.background.testhelpers.CommandHelpers; +import org.mozilla.gecko.sync.CommandProcessor.Command; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.repositories.NullCursorException; +import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; +import org.mozilla.gecko.sync.repositories.domain.ClientRecord; + +import android.content.Context; +import android.database.Cursor; +import android.test.AndroidTestCase; + +public class TestClientsDatabaseAccessor extends AndroidTestCase { + + public class StubbedClientsDatabaseAccessor extends ClientsDatabaseAccessor { + public StubbedClientsDatabaseAccessor(Context mContext) { + super(mContext); + } + } + + StubbedClientsDatabaseAccessor db; + + public void setUp() { + db = new StubbedClientsDatabaseAccessor(mContext); + db.wipeDB(); + } + + public void tearDown() { + db.close(); + } + + public void testStoreArrayListAndFetch() throws NullCursorException { + ArrayList<ClientRecord> list = new ArrayList<ClientRecord>(); + ClientRecord record1 = new ClientRecord(Utils.generateGuid()); + ClientRecord record2 = new ClientRecord(Utils.generateGuid()); + ClientRecord record3 = new ClientRecord(Utils.generateGuid()); + + list.add(record1); + list.add(record2); + db.store(list); + + ClientRecord r1 = db.fetchClient(record1.guid); + ClientRecord r2 = db.fetchClient(record2.guid); + ClientRecord r3 = db.fetchClient(record3.guid); + + assertNotNull(r1); + assertNotNull(r2); + assertNull(r3); + assertTrue(record1.equals(r1)); + assertTrue(record2.equals(r2)); + assertFalse(record3.equals(r3)); + } + + public void testStoreAndFetchCommandsForClient() { + String accountGUID1 = Utils.generateGuid(); + String accountGUID2 = Utils.generateGuid(); + + Command command1 = CommandHelpers.getCommand1(); + Command command2 = CommandHelpers.getCommand2(); + Command command3 = CommandHelpers.getCommand3(); + + Cursor cur = null; + try { + db.store(accountGUID1, command1); + db.store(accountGUID1, command2); + db.store(accountGUID2, command3); + + List<Command> commands = db.fetchCommandsForClient(accountGUID1); + assertEquals(2, commands.size()); + assertEquals(1, commands.get(0).args.size()); + assertEquals(1, commands.get(1).args.size()); + } catch (NullCursorException e) { + fail("Should not have NullCursorException"); + } finally { + if (cur != null) { + cur.close(); + } + } + } + + public void testNumClients() { + final int COUNT = 5; + ArrayList<ClientRecord> list = new ArrayList<ClientRecord>(); + for (int i = 0; i < 5; i++) { + list.add(new ClientRecord()); + } + db.store(list); + assertEquals(COUNT, db.clientsCount()); + } + + public void testFetchAll() throws NullCursorException { + ArrayList<ClientRecord> list = new ArrayList<ClientRecord>(); + ClientRecord record1 = new ClientRecord(Utils.generateGuid()); + ClientRecord record2 = new ClientRecord(Utils.generateGuid()); + + list.add(record1); + list.add(record2); + + boolean thrown = false; + try { + Map<String, ClientRecord> records = db.fetchAllClients(); + + assertNotNull(records); + assertEquals(0, records.size()); + + db.store(list); + records = db.fetchAllClients(); + assertNotNull(records); + assertEquals(2, records.size()); + assertTrue(record1.equals(records.get(record1.guid))); + assertTrue(record2.equals(records.get(record2.guid))); + + // put() should throw an exception since records is immutable. + records.put(null, null); + } catch (UnsupportedOperationException e) { + thrown = true; + } + assertTrue(thrown); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java new file mode 100644 index 000000000..02d393ce8 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java @@ -0,0 +1,297 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import org.json.simple.JSONArray; +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate; +import org.mozilla.gecko.background.sync.helpers.SessionTestHelper; +import org.mozilla.gecko.background.testhelpers.MockClientsDataDelegate; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserContract.Clients; +import org.mozilla.gecko.sync.repositories.NoContentProviderException; +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers; +import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; +import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository; +import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository.FennecTabsRepositorySession; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; +import org.mozilla.gecko.sync.repositories.domain.ClientRecord; +import org.mozilla.gecko.sync.repositories.domain.Record; +import org.mozilla.gecko.sync.repositories.domain.TabsRecord; + +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.os.RemoteException; + +public class TestFennecTabsRepositorySession extends AndroidSyncTestCase { + public static final MockClientsDataDelegate clientsDataDelegate = new MockClientsDataDelegate(); + public static final String TEST_CLIENT_GUID = clientsDataDelegate.getAccountGUID(); + public static final String TEST_CLIENT_NAME = clientsDataDelegate.getClientName(); + public static final String TEST_CLIENT_DEVICE_TYPE = "phablet"; + + // Override these to test against data that is not live. + public static final String TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS ?"; + public static final String[] TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS = new String[] { TEST_CLIENT_GUID }; + + public static final String TEST_CLIENTS_GUID_IS_LOCAL_SELECTION = BrowserContract.Clients.GUID + " IS ?"; + public static final String[] TEST_CLIENTS_GUID_IS_LOCAL_SELECTION_ARGS = new String[] { TEST_CLIENT_GUID }; + + protected ContentProviderClient tabsClient = null; + protected ContentProviderClient clientsClient = null; + + protected ContentProviderClient getTabsClient() { + final ContentResolver cr = getApplicationContext().getContentResolver(); + return cr.acquireContentProviderClient(BrowserContractHelpers.TABS_CONTENT_URI); + } + + protected ContentProviderClient getClientsClient() { + final ContentResolver cr = getApplicationContext().getContentResolver(); + return cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI); + } + + public TestFennecTabsRepositorySession() throws NoContentProviderException { + super(); + } + + @Override + public void setUp() { + if (tabsClient == null) { + tabsClient = getTabsClient(); + } + if (clientsClient == null) { + clientsClient = getClientsClient(); + } + } + + protected int deleteTestClient(final ContentProviderClient clientsClient) throws RemoteException { + if (clientsClient == null) { + return -1; + } + return clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, TEST_CLIENTS_GUID_IS_LOCAL_SELECTION, TEST_CLIENTS_GUID_IS_LOCAL_SELECTION_ARGS); + } + + protected int deleteAllTestTabs(final ContentProviderClient tabsClient) throws RemoteException { + if (tabsClient == null) { + return -1; + } + return tabsClient.delete(BrowserContractHelpers.TABS_CONTENT_URI, + TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION, TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS); + } + + @Override + protected void tearDown() throws Exception { + if (tabsClient != null) { + deleteAllTestTabs(tabsClient); + + tabsClient.release(); + tabsClient = null; + } + + if (clientsClient != null) { + deleteTestClient(clientsClient); + + clientsClient.release(); + clientsClient = null; + } + } + + protected FennecTabsRepository getRepository() { + /** + * Override this chain in order to avoid our test code having to create two + * sessions all the time. + */ + return new FennecTabsRepository(clientsDataDelegate) { + @Override + public void createSession(RepositorySessionCreationDelegate delegate, + Context context) { + try { + final FennecTabsRepositorySession session = new FennecTabsRepositorySession(this, context) { + @Override + protected synchronized void trackGUID(String guid) { + } + + @Override + protected String localClientSelection() { + return TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION; + } + + @Override + protected String[] localClientSelectionArgs() { + return TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS; + } + }; + delegate.onSessionCreated(session); + } catch (Exception e) { + delegate.onSessionCreateFailed(e); + } + } + }; + } + + protected FennecTabsRepositorySession createSession() { + return (FennecTabsRepositorySession) SessionTestHelper.createSession( + getApplicationContext(), + getRepository()); + } + + protected FennecTabsRepositorySession createAndBeginSession() { + return (FennecTabsRepositorySession) SessionTestHelper.createAndBeginSession( + getApplicationContext(), + getRepository()); + } + + protected Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final Record[] expectedRecords) { + return new Runnable() { + @Override + public void run() { + session.fetchSince(timestamp, new ExpectFetchDelegate(expectedRecords)); + } + }; + } + + protected Runnable fetchAllRunnable(final RepositorySession session, final Record[] expectedRecords) { + return new Runnable() { + @Override + public void run() { + session.fetchAll(new ExpectFetchDelegate(expectedRecords)); + } + }; + } + + protected Tab testTab1; + protected Tab testTab2; + protected Tab testTab3; + + @SuppressWarnings("unchecked") + private void insertSomeTestTabs(ContentProviderClient tabsClient) throws RemoteException { + final JSONArray history1 = new JSONArray(); + history1.add("http://test.com/test1.html"); + testTab1 = new Tab("test title 1", "http://test.com/test1.png", history1, 1000); + + final JSONArray history2 = new JSONArray(); + history2.add("http://test.com/test2.html#1"); + history2.add("http://test.com/test2.html#2"); + history2.add("http://test.com/test2.html#3"); + testTab2 = new Tab("test title 2", "http://test.com/test2.png", history2, 2000); + + final JSONArray history3 = new JSONArray(); + history3.add("http://test.com/test3.html#1"); + history3.add("http://test.com/test3.html#2"); + testTab3 = new Tab("test title 3", "http://test.com/test3.png", history3, 3000); + + tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab1.toContentValues(TEST_CLIENT_GUID, 0)); + tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab2.toContentValues(TEST_CLIENT_GUID, 1)); + tabsClient.insert(BrowserContractHelpers.TABS_CONTENT_URI, testTab3.toContentValues(TEST_CLIENT_GUID, 2)); + } + + protected TabsRecord insertTestTabsAndExtractTabsRecord() throws RemoteException { + insertSomeTestTabs(tabsClient); + + final String positionAscending = BrowserContract.Tabs.POSITION + " ASC"; + Cursor cursor = null; + try { + cursor = tabsClient.query(BrowserContractHelpers.TABS_CONTENT_URI, null, + TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION, TEST_TABS_CLIENT_GUID_IS_LOCAL_SELECTION_ARGS, positionAscending); + CursorDumper.dumpCursor(cursor); + + final TabsRecord tabsRecord = FennecTabsRepository.tabsRecordFromCursor(cursor, TEST_CLIENT_GUID, TEST_CLIENT_NAME); + + assertEquals(TEST_CLIENT_GUID, tabsRecord.guid); + assertEquals(TEST_CLIENT_NAME, tabsRecord.clientName); + + assertNotNull(tabsRecord.tabs); + assertEquals(cursor.getCount(), tabsRecord.tabs.size()); + + return tabsRecord; + } finally { + cursor.close(); + } + } + + public void testFetchAll() throws NoContentProviderException, RemoteException { + final TabsRecord tabsRecord = insertTestTabsAndExtractTabsRecord(); + + final FennecTabsRepositorySession session = createAndBeginSession(); + performWait(fetchAllRunnable(session, new Record[] { tabsRecord })); + + session.abort(); + } + + public void testFetchSince() throws NoContentProviderException, RemoteException { + final TabsRecord tabsRecord = insertTestTabsAndExtractTabsRecord(); + + final FennecTabsRepositorySession session = createAndBeginSession(); + + // Not all tabs are modified after this, but the record should contain them all. + performWait(fetchSinceRunnable(session, 1000, new Record[] { tabsRecord })); + + // No tabs are modified after this, but our client name has changed in the interim. + performWait(fetchSinceRunnable(session, 4000, new Record[] { tabsRecord })); + + // No tabs are modified after this, and our client name hasn't changed, so + // we shouldn't get a record at all. Note: this runs after our static + // initializer that sets the client data timestamp. + final long now = System.currentTimeMillis(); + performWait(fetchSinceRunnable(session, now, new Record[] { })); + + // No tabs are modified after this, but our client name has changed, so + // again we get a record. + clientsDataDelegate.setClientName("new client name", System.currentTimeMillis()); + performWait(fetchSinceRunnable(session, now, new Record[] { tabsRecord })); + + session.abort(); + } + + // Verify that storing a tabs record writes a clients record with the correct + // device type to the Fennec clients provider. + public void testStore() throws NoContentProviderException, RemoteException { + // Get a valid tabsRecord to write. + final TabsRecord tabsRecord = insertTestTabsAndExtractTabsRecord(); + deleteAllTestTabs(tabsClient); + deleteTestClient(clientsClient); + + final ContentResolver cr = getApplicationContext().getContentResolver(); + final ContentProviderClient clientsClient = cr.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI); + + try { + // This clients DB is not the Fennec DB; it's Sync's own clients DB. + final ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(getApplicationContext()); + try { + ClientRecord clientRecord = new ClientRecord(TEST_CLIENT_GUID); + clientRecord.name = TEST_CLIENT_NAME; + clientRecord.type = TEST_CLIENT_DEVICE_TYPE; + db.store(clientRecord); + } finally { + db.close(); + } + + final FennecTabsRepositorySession session = createAndBeginSession(); + performWait(AndroidBrowserRepositoryTestCase.storeRunnable(session, tabsRecord)); + + session.abort(); + + // This store should write Sync's idea of the client's device_type to Fennec's clients CP. + final Cursor cursor = clientsClient.query(BrowserContractHelpers.CLIENTS_CONTENT_URI, null, + TEST_CLIENTS_GUID_IS_LOCAL_SELECTION, TEST_CLIENTS_GUID_IS_LOCAL_SELECTION_ARGS, null); + assertNotNull(cursor); + + try { + assertTrue(cursor.moveToFirst()); + assertEquals(TEST_CLIENT_GUID, cursor.getString(cursor.getColumnIndex(Clients.GUID))); + assertEquals(TEST_CLIENT_NAME, cursor.getString(cursor.getColumnIndex(Clients.NAME))); + assertEquals(TEST_CLIENT_DEVICE_TYPE, cursor.getString(cursor.getColumnIndex(Clients.DEVICE_TYPE))); + assertTrue(cursor.isLast()); + } finally { + cursor.close(); + } + } finally { + // We can't delete only our test client due to a Fennec CP issue with guid vs. client_guid. + clientsClient.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, null, null); + clientsClient.release(); + } + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java new file mode 100644 index 000000000..5d5014b75 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java @@ -0,0 +1,441 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import java.util.concurrent.ExecutorService; + +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectNoStoreDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectStoredDelegate; +import org.mozilla.gecko.background.sync.helpers.SessionTestHelper; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.sync.repositories.InactiveSessionException; +import org.mozilla.gecko.sync.repositories.NoContentProviderException; +import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers; +import org.mozilla.gecko.sync.repositories.android.FormHistoryRepositorySession; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; +import org.mozilla.gecko.sync.repositories.domain.FormHistoryRecord; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; + +public class TestFormHistoryRepositorySession extends AndroidSyncTestCase { + protected ContentProviderClient formsProvider = null; + + public TestFormHistoryRepositorySession() throws NoContentProviderException { + super(); + } + + public void setUp() { + if (formsProvider == null) { + try { + formsProvider = FormHistoryRepositorySession.acquireContentProvider(getApplicationContext()); + } catch (NoContentProviderException e) { + fail("Failed to acquireContentProvider: " + e); + } + } + + try { + FormHistoryRepositorySession.purgeDatabases(formsProvider); + } catch (RemoteException e) { + fail("Failed to purgeDatabases: " + e); + } + } + + public void tearDown() { + if (formsProvider != null) { + formsProvider.release(); + formsProvider = null; + } + } + + protected FormHistoryRepositorySession.FormHistoryRepository getRepository() { + /** + * Override this chain in order to avoid our test code having to create two + * sessions all the time. + */ + return new FormHistoryRepositorySession.FormHistoryRepository() { + @Override + public void createSession(RepositorySessionCreationDelegate delegate, + Context context) { + try { + final FormHistoryRepositorySession session = new FormHistoryRepositorySession(this, context) { + @Override + protected synchronized void trackGUID(String guid) { + } + }; + delegate.onSessionCreated(session); + } catch (Exception e) { + delegate.onSessionCreateFailed(e); + } + } + }; + } + + + protected FormHistoryRepositorySession createSession() { + return (FormHistoryRepositorySession) SessionTestHelper.createSession( + getApplicationContext(), + getRepository()); + } + + protected FormHistoryRepositorySession createAndBeginSession() { + return (FormHistoryRepositorySession) SessionTestHelper.createAndBeginSession( + getApplicationContext(), + getRepository()); + } + + public void testAcquire() throws NoContentProviderException { + final FormHistoryRepositorySession session = createAndBeginSession(); + assertNotNull(session.getFormsProvider()); + session.abort(); + } + + protected int numRecords(FormHistoryRepositorySession session, Uri uri) throws RemoteException { + Cursor cur = null; + try { + cur = session.getFormsProvider().query(uri, null, null, null, null); + return cur.getCount(); + } finally { + if (cur != null) { + cur.close(); + } + } + } + + protected long after0; + protected long after1; + protected long after2; + protected long after3; + protected long after4; + protected FormHistoryRecord regular1; + protected FormHistoryRecord regular2; + protected FormHistoryRecord deleted1; + protected FormHistoryRecord deleted2; + + public void insertTwoRecords(FormHistoryRepositorySession session) throws RemoteException { + Uri regularUri = BrowserContractHelpers.FORM_HISTORY_CONTENT_URI; + Uri deletedUri = BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI; + after0 = System.currentTimeMillis(); + + regular1 = new FormHistoryRecord("guid1", "forms", System.currentTimeMillis(), false); + regular1.fieldName = "fieldName1"; + regular1.fieldValue = "value1"; + final ContentValues cv1 = new ContentValues(); + cv1.put(BrowserContract.FormHistory.GUID, regular1.guid); + cv1.put(BrowserContract.FormHistory.FIELD_NAME, regular1.fieldName); + cv1.put(BrowserContract.FormHistory.VALUE, regular1.fieldValue); + cv1.put(BrowserContract.FormHistory.FIRST_USED, 1000 * regular1.lastModified); // Microseconds. + + int regularInserted = session.getFormsProvider().bulkInsert(regularUri, new ContentValues[] { cv1 }); + assertEquals(1, regularInserted); + after1 = System.currentTimeMillis(); + + deleted1 = new FormHistoryRecord("guid3", "forms", -1, true); + final ContentValues cv3 = new ContentValues(); + cv3.put(BrowserContract.FormHistory.GUID, deleted1.guid); + // cv3.put(BrowserContract.DeletedFormHistory.TIME_DELETED, record3.lastModified); // Set by CP. + + int deletedInserted = session.getFormsProvider().bulkInsert(deletedUri, new ContentValues[] { cv3 }); + assertEquals(1, deletedInserted); + after2 = System.currentTimeMillis(); + + regular2 = null; + deleted2 = null; + } + + public void insertFourRecords(FormHistoryRepositorySession session) throws RemoteException { + Uri regularUri = BrowserContractHelpers.FORM_HISTORY_CONTENT_URI; + Uri deletedUri = BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI; + + insertTwoRecords(session); + + regular2 = new FormHistoryRecord("guid2", "forms", System.currentTimeMillis(), false); + regular2.fieldName = "fieldName2"; + regular2.fieldValue = "value2"; + final ContentValues cv2 = new ContentValues(); + cv2.put(BrowserContract.FormHistory.GUID, regular2.guid); + cv2.put(BrowserContract.FormHistory.FIELD_NAME, regular2.fieldName); + cv2.put(BrowserContract.FormHistory.VALUE, regular2.fieldValue); + cv2.put(BrowserContract.FormHistory.FIRST_USED, 1000 * regular2.lastModified); // Microseconds. + + int regularInserted = session.getFormsProvider().bulkInsert(regularUri, new ContentValues[] { cv2 }); + assertEquals(1, regularInserted); + after3 = System.currentTimeMillis(); + + deleted2 = new FormHistoryRecord("guid4", "forms", -1, true); + final ContentValues cv4 = new ContentValues(); + cv4.put(BrowserContract.FormHistory.GUID, deleted2.guid); + // cv4.put(BrowserContract.DeletedFormHistory.TIME_DELETED, record4.lastModified); // Set by CP. + + int deletedInserted = session.getFormsProvider().bulkInsert(deletedUri, new ContentValues[] { cv4 }); + assertEquals(1, deletedInserted); + after4 = System.currentTimeMillis(); + } + + public void testWipe() throws NoContentProviderException, RemoteException { + final FormHistoryRepositorySession session = createAndBeginSession(); + + insertTwoRecords(session); + assertTrue(numRecords(session, BrowserContractHelpers.FORM_HISTORY_CONTENT_URI) > 0); + assertTrue(numRecords(session, BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI) > 0); + + performWait(WaitHelper.onThreadRunnable(new Runnable() { + @Override + public void run() { + session.wipe(new RepositorySessionWipeDelegate() { + public void onWipeSucceeded() { + performNotify(); + } + public void onWipeFailed(Exception ex) { + performNotify("Wipe should have succeeded", ex); + } + @Override + public RepositorySessionWipeDelegate deferredWipeDelegate(final ExecutorService executor) { + return this; + } + }); + } + })); + + assertEquals(0, numRecords(session, BrowserContractHelpers.FORM_HISTORY_CONTENT_URI)); + assertEquals(0, numRecords(session, BrowserContractHelpers.DELETED_FORM_HISTORY_CONTENT_URI)); + + session.abort(); + } + + protected Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final String[] expectedGuids) { + return new Runnable() { + @Override + public void run() { + session.fetchSince(timestamp, new ExpectFetchSinceDelegate(timestamp, expectedGuids)); + } + }; + } + + protected Runnable fetchAllRunnable(final RepositorySession session, final Record[] expectedRecords) { + return new Runnable() { + @Override + public void run() { + session.fetchAll(new ExpectFetchDelegate(expectedRecords)); + } + }; + } + + protected Runnable fetchRunnable(final RepositorySession session, final String[] guids, final Record[] expectedRecords) { + return new Runnable() { + @Override + public void run() { + try { + session.fetch(guids, new ExpectFetchDelegate(expectedRecords)); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + }; + } + + public void testFetchAll() throws NoContentProviderException, RemoteException { + final FormHistoryRepositorySession session = createAndBeginSession(); + + insertTwoRecords(session); + + performWait(fetchAllRunnable(session, new Record[] { regular1, deleted1 })); + + session.abort(); + } + + public void testFetchByGuid() throws NoContentProviderException, RemoteException { + final FormHistoryRepositorySession session = createAndBeginSession(); + + insertTwoRecords(session); + + performWait(fetchRunnable(session, + new String[] { regular1.guid, deleted1.guid }, + new Record[] { regular1, deleted1 })); + performWait(fetchRunnable(session, + new String[] { regular1.guid }, + new Record[] { regular1 })); + performWait(fetchRunnable(session, + new String[] { deleted1.guid, "NON_EXISTENT_GUID?" }, + new Record[] { deleted1 })); + performWait(fetchRunnable(session, + new String[] { "FIRST_NON_EXISTENT_GUID", "SECOND_NON_EXISTENT_GUID?" }, + new Record[] { })); + + session.abort(); + } + + public void testFetchSince() throws NoContentProviderException, RemoteException { + final FormHistoryRepositorySession session = createAndBeginSession(); + + insertFourRecords(session); + + performWait(fetchSinceRunnable(session, + after0, new String[] { regular1.guid, deleted1.guid, regular2.guid, deleted2.guid })); + performWait(fetchSinceRunnable(session, + after1, new String[] { deleted1.guid, regular2.guid, deleted2.guid })); + performWait(fetchSinceRunnable(session, + after2, new String[] { regular2.guid, deleted2.guid })); + performWait(fetchSinceRunnable(session, + after3, new String[] { deleted2.guid })); + performWait(fetchSinceRunnable(session, + after4, new String[] { })); + + session.abort(); + } + + protected Runnable guidsSinceRunnable(final RepositorySession session, final long timestamp, final String[] expectedGuids) { + return new Runnable() { + @Override + public void run() { + session.guidsSince(timestamp, new ExpectGuidsSinceDelegate(expectedGuids)); + } + }; + } + + public void testGuidsSince() throws NoContentProviderException, RemoteException { + final FormHistoryRepositorySession session = createAndBeginSession(); + + insertTwoRecords(session); + + performWait(guidsSinceRunnable(session, + after0, new String[] { regular1.guid, deleted1.guid })); + performWait(guidsSinceRunnable(session, + after1, new String[] { deleted1.guid})); + performWait(guidsSinceRunnable(session, + after2, new String[] { })); + + session.abort(); + } + + protected Runnable storeRunnable(final RepositorySession session, final Record record, final RepositorySessionStoreDelegate delegate) { + return new Runnable() { + @Override + public void run() { + session.setStoreDelegate(delegate); + try { + session.store(record); + session.storeDone(); + } catch (NoStoreDelegateException e) { + performNotify("NoStoreDelegateException should not occur.", e); + } + } + }; + } + + public void testStoreRemoteNew() throws NoContentProviderException, RemoteException { + final FormHistoryRepositorySession session = createAndBeginSession(); + + insertTwoRecords(session); + + FormHistoryRecord rec; + + // remote regular, local missing => should store. + rec = new FormHistoryRecord("new1", "forms", System.currentTimeMillis(), false); + rec.fieldName = "fieldName1"; + rec.fieldValue = "fieldValue1"; + performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid))); + performWait(fetchRunnable(session, new String[] { rec.guid }, new Record[] { rec })); + + // remote deleted, local missing => should delete, but at the moment we ignore. + rec = new FormHistoryRecord("new2", "forms", System.currentTimeMillis(), true); + performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate())); + performWait(fetchRunnable(session, new String[] { rec.guid }, new Record[] { })); + + session.abort(); + } + + public void testStoreRemoteNewer() throws NoContentProviderException, RemoteException { + final FormHistoryRepositorySession session = createAndBeginSession(); + + insertFourRecords(session); + long newTimestamp = System.currentTimeMillis(); + + FormHistoryRecord rec; + + // remote regular, local regular, remote newer => should update. + rec = new FormHistoryRecord(regular1.guid, regular1.collection, newTimestamp, false); + rec.fieldName = regular1.fieldName; + rec.fieldValue = regular1.fieldValue + "NEW"; + performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid))); + performWait(fetchRunnable(session, new String[] { regular1.guid }, new Record[] { rec })); + + // remote deleted, local regular, remote newer => should delete everything. + rec = new FormHistoryRecord(regular2.guid, regular2.collection, newTimestamp, true); + performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid))); + performWait(fetchRunnable(session, new String[] { regular2.guid }, new Record[] { })); + + // remote regular, local deleted, remote newer => should update. + rec = new FormHistoryRecord(deleted1.guid, deleted1.collection, newTimestamp, false); + rec.fieldName = regular1.fieldName; + rec.fieldValue = regular1.fieldValue + "NEW"; + performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid))); + performWait(fetchRunnable(session, new String[] { deleted1.guid }, new Record[] { rec })); + + // remote deleted, local deleted, remote newer => should delete everything. + rec = new FormHistoryRecord(deleted2.guid, deleted2.collection, newTimestamp, true); + performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate())); + performWait(fetchRunnable(session, new String[] { deleted2.guid }, new Record[] { })); + + session.abort(); + } + + public void testStoreRemoteOlder() throws NoContentProviderException, RemoteException { + final FormHistoryRepositorySession session = createAndBeginSession(); + + long oldTimestamp = System.currentTimeMillis() - 100; + insertFourRecords(session); + + FormHistoryRecord rec; + + // remote regular, local regular, remote older => should ignore. + rec = new FormHistoryRecord(regular1.guid, regular1.collection, oldTimestamp, false); + rec.fieldName = regular1.fieldName; + rec.fieldValue = regular1.fieldValue + "NEW"; + performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate())); + + // remote deleted, local regular, remote older => should ignore. + rec = new FormHistoryRecord(regular2.guid, regular2.collection, oldTimestamp, true); + performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate())); + + // remote regular, local deleted, remote older => should ignore. + rec = new FormHistoryRecord(deleted1.guid, deleted1.collection, oldTimestamp, false); + rec.fieldName = regular1.fieldName; + rec.fieldValue = regular1.fieldValue + "NEW"; + performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate())); + + // remote deleted, local deleted, remote older => should ignore. + rec = new FormHistoryRecord(deleted2.guid, deleted2.collection, oldTimestamp, true); + performWait(storeRunnable(session, rec, new ExpectNoStoreDelegate())); + + session.abort(); + } + + public void testStoreDifferentGuid() throws NoContentProviderException, RemoteException { + final FormHistoryRepositorySession session = createAndBeginSession(); + + insertTwoRecords(session); + + FormHistoryRecord rec = (FormHistoryRecord) regular1.copyWithIDs("distinct", 999); + performWait(storeRunnable(session, rec, new ExpectStoredDelegate(rec.guid))); + // Existing record should take remote record's GUID. + performWait(fetchAllRunnable(session, new Record[] { rec, deleted1 })); + + session.abort(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestPasswordsRepository.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestPasswordsRepository.java new file mode 100644 index 000000000..210c8ca8c --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestPasswordsRepository.java @@ -0,0 +1,482 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + +import java.util.HashSet; +import java.util.Set; + +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectGuidsSinceDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectNoStoreDelegate; +import org.mozilla.gecko.background.sync.helpers.ExpectStoredDelegate; +import org.mozilla.gecko.background.sync.helpers.PasswordHelpers; +import org.mozilla.gecko.background.sync.helpers.SessionTestHelper; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +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.NoStoreDelegateException; +import org.mozilla.gecko.sync.repositories.Repository; +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers; +import org.mozilla.gecko.sync.repositories.android.PasswordsRepositorySession; +import org.mozilla.gecko.sync.repositories.android.RepoUtils; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; +import org.mozilla.gecko.sync.repositories.domain.PasswordRecord; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.os.RemoteException; + +public class TestPasswordsRepository extends AndroidSyncTestCase { + private final String NEW_PASSWORD1 = "password"; + private final String NEW_PASSWORD2 = "drowssap"; + + @Override + public void setUp() { + wipe(); + assertTrue(WaitHelper.getTestWaiter().isIdle()); + } + + public void testFetchAll() { + RepositorySession session = createAndBeginSession(); + Record[] expected = new Record[] { PasswordHelpers.createPassword1(), + PasswordHelpers.createPassword2() }; + + performWait(storeRunnable(session, expected[0])); + performWait(storeRunnable(session, expected[1])); + + performWait(fetchAllRunnable(session, expected)); + dispose(session); + } + + public void testGuidsSinceReturnMultipleRecords() { + RepositorySession session = createAndBeginSession(); + + PasswordRecord record1 = PasswordHelpers.createPassword1(); + PasswordRecord record2 = PasswordHelpers.createPassword2(); + + updatePassword(NEW_PASSWORD1, record1); + long timestamp = updatePassword(NEW_PASSWORD2, record2); + + String[] expected = new String[] { record1.guid, record2.guid }; + + performWait(storeRunnable(session, record1)); + performWait(storeRunnable(session, record2)); + + performWait(guidsSinceRunnable(session, timestamp, expected)); + dispose(session); + } + + public void testGuidsSinceReturnNoRecords() { + RepositorySession session = createAndBeginSession(); + + // Store 1 record in the past. + performWait(storeRunnable(session, PasswordHelpers.createPassword1())); + + String[] expected = {}; + performWait(guidsSinceRunnable(session, System.currentTimeMillis() + 1000, expected)); + dispose(session); + } + + public void testFetchSinceOneRecord() { + RepositorySession session = createAndBeginSession(); + + // Passwords fetchSince checks timePasswordChanged, not insertion time. + PasswordRecord record1 = PasswordHelpers.createPassword1(); + long timeModified1 = updatePassword(NEW_PASSWORD1, record1); + performWait(storeRunnable(session, record1)); + + PasswordRecord record2 = PasswordHelpers.createPassword2(); + long timeModified2 = updatePassword(NEW_PASSWORD2, record2); + performWait(storeRunnable(session, record2)); + + String[] expectedOne = new String[] { record2.guid }; + performWait(fetchSinceRunnable(session, timeModified2 - 10, expectedOne)); + + String[] expectedBoth = new String[] { record1.guid, record2.guid }; + performWait(fetchSinceRunnable(session, timeModified1 - 10, expectedBoth)); + + dispose(session); + } + + public void testFetchSinceReturnNoRecords() { + RepositorySession session = createAndBeginSession(); + + performWait(storeRunnable(session, PasswordHelpers.createPassword2())); + + long timestamp = System.currentTimeMillis(); + + performWait(fetchSinceRunnable(session, timestamp + 2000, new String[] {})); + dispose(session); + } + + public void testFetchOneRecordByGuid() { + RepositorySession session = createAndBeginSession(); + Record record = PasswordHelpers.createPassword1(); + performWait(storeRunnable(session, record)); + performWait(storeRunnable(session, PasswordHelpers.createPassword2())); + + String[] guids = new String[] { record.guid }; + Record[] expected = new Record[] { record }; + performWait(fetchRunnable(session, guids, expected)); + dispose(session); + } + + public void testFetchMultipleRecordsByGuids() { + RepositorySession session = createAndBeginSession(); + PasswordRecord record1 = PasswordHelpers.createPassword1(); + PasswordRecord record2 = PasswordHelpers.createPassword2(); + PasswordRecord record3 = PasswordHelpers.createPassword3(); + + performWait(storeRunnable(session, record1)); + performWait(storeRunnable(session, record2)); + performWait(storeRunnable(session, record3)); + + String[] guids = new String[] { record1.guid, record2.guid }; + Record[] expected = new Record[] { record1, record2 }; + performWait(fetchRunnable(session, guids, expected)); + dispose(session); + } + + public void testFetchNoRecordByGuid() { + RepositorySession session = createAndBeginSession(); + Record record = PasswordHelpers.createPassword1(); + + performWait(storeRunnable(session, record)); + performWait(fetchRunnable(session, + new String[] { Utils.generateGuid() }, + new Record[] {})); + dispose(session); + } + + public void testStore() { + final RepositorySession session = createAndBeginSession(); + performWait(storeRunnable(session, PasswordHelpers.createPassword1())); + dispose(session); + } + + public void testRemoteNewerTimeStamp() { + final RepositorySession session = createAndBeginSession(); + + // Store updated local record. + PasswordRecord local = PasswordHelpers.createPassword1(); + updatePassword(NEW_PASSWORD1, local, System.currentTimeMillis() - 1000); + performWait(storeRunnable(session, local)); + + // Sync a remote record version that is newer. + PasswordRecord remote = PasswordHelpers.createPassword2(); + remote.guid = local.guid; + updatePassword(NEW_PASSWORD2, remote); + performWait(storeRunnable(session, remote)); + + // Make a fetch, expecting only the newer (remote) record. + performWait(fetchAllRunnable(session, new Record[] { remote })); + + // Store an older local record. + PasswordRecord local2 = PasswordHelpers.createPassword3(); + updatePassword(NEW_PASSWORD2, local2, System.currentTimeMillis() - 1000); + performWait(storeRunnable(session, local2)); + + // Sync a remote record version that is newer and is deleted. + PasswordRecord remote2 = PasswordHelpers.createPassword3(); + remote2.guid = local2.guid; + remote2.deleted = true; + updatePassword(NEW_PASSWORD2, remote2); + performWait(storeRunnable(session, remote2)); + + // Make a fetch, expecting the local record to be deleted. + performWait(fetchRunnable(session, new String[] { remote2.guid }, new Record[] {})); + + // Store an older deleted local record. + PasswordRecord local3 = PasswordHelpers.createPassword4(); + updatePassword(NEW_PASSWORD2, local3, System.currentTimeMillis() - 1000); + local3.deleted = true; + storeLocalDeletedRecord(local3, System.currentTimeMillis() - 1000); + + // Sync a remote record version that is newer and is deleted. + PasswordRecord remote3 = PasswordHelpers.createPassword5(); + remote3.guid = local3.guid; + remote3.deleted = true; + updatePassword(NEW_PASSWORD2, remote3); + performWait(storeRunnable(session, remote3)); + + // Make a fetch, expecting the local record to be deleted. + performWait(fetchRunnable(session, new String[] { remote3.guid }, new Record[] {})); + dispose(session); + } + + public void testLocalNewerTimeStamp() { + final RepositorySession session = createAndBeginSession(); + // Remote record updated before local record. + PasswordRecord remote = PasswordHelpers.createPassword1(); + updatePassword(NEW_PASSWORD1, remote, System.currentTimeMillis() - 1000); + + // Store updated local record. + PasswordRecord local = PasswordHelpers.createPassword2(); + updatePassword(NEW_PASSWORD2, local); + performWait(storeRunnable(session, local)); + + // Sync a remote record version that is older. + remote.guid = local.guid; + performWait(storeRunnable(session, remote)); + + // Make a fetch, expecting only the newer (local) record. + performWait(fetchAllRunnable(session, new Record[] { local })); + + // Remote record updated before local record. + PasswordRecord remote2 = PasswordHelpers.createPassword3(); + updatePassword(NEW_PASSWORD1, remote2, System.currentTimeMillis() - 1000); + + // Store updated local record that is deleted. + PasswordRecord local2 = PasswordHelpers.createPassword3(); + updatePassword(NEW_PASSWORD2, local2); + local2.deleted = true; + storeLocalDeletedRecord(local2, System.currentTimeMillis()); + + // Sync a remote record version that is older. + remote2.guid = local2.guid; + performWait(storeRunnable(session, remote2, new ExpectNoStoreDelegate())); + + // Make a fetch, expecting only the deleted newer (local) record. + performWait(fetchRunnable(session, new String[] { local2.guid }, new Record[] { local2 })); + + // Remote record updated before local record. + PasswordRecord remote3 = PasswordHelpers.createPassword4(); + updatePassword(NEW_PASSWORD1, remote3, System.currentTimeMillis() - 1000); + + // Store updated local record that is deleted. + PasswordRecord local3 = PasswordHelpers.createPassword4(); + updatePassword(NEW_PASSWORD2, local3); + local3.deleted = true; + storeLocalDeletedRecord(local3, System.currentTimeMillis()); + + // Sync a remote record version that is older and is deleted. + remote3.guid = local3.guid; + remote3.deleted = true; + performWait(storeRunnable(session, remote3)); + + // Make a fetch, expecting the local record to be deleted. + performWait(fetchRunnable(session, new String[] { local3.guid }, new Record[] {})); + dispose(session); + } + + /* + * Store two records that are identical except for guid. Expect to find the + * remote one after reconciling. + */ + public void testStoreIdenticalExceptGuid() { + RepositorySession session = createAndBeginSession(); + PasswordRecord record = PasswordHelpers.createPassword1(); + record.guid = "before1"; + // Store record. + performWait(storeRunnable(session, record)); + + // Store same record, but with different guid. + record.guid = Utils.generateGuid(); + performWait(storeRunnable(session, record)); + + performWait(fetchAllRunnable(session, new Record[] { record })); + dispose(session); + + session = createAndBeginSession(); + + PasswordRecord record2 = PasswordHelpers.createPassword2(); + record2.guid = "before2"; + // Store record. + performWait(storeRunnable(session, record2)); + + // Store same record, but with different guid. + record2.guid = Utils.generateGuid(); + performWait(storeRunnable(session, record2)); + + performWait(fetchAllRunnable(session, new Record[] { record, record2 })); + dispose(session); + } + + /* + * Store two records that are identical except for guid when they both point + * to the same site and there are multiple records for that site. Expect to + * find the remote one after reconciling. + */ + public void testStoreIdenticalExceptGuidOnSameSite() { + RepositorySession session = createAndBeginSession(); + PasswordRecord record1 = PasswordHelpers.createPassword1(); + record1.encryptedUsername = "original"; + record1.guid = "before1"; + PasswordRecord record2 = PasswordHelpers.createPassword1(); + record2.encryptedUsername = "different"; + record1.guid = "before2"; + // Store records. + performWait(storeRunnable(session, record1)); + performWait(storeRunnable(session, record2)); + performWait(fetchAllRunnable(session, new Record[] { record1, record2 })); + + dispose(session); + session = createAndBeginSession(); + + // Store same records, but with different guids. + record1.guid = Utils.generateGuid(); + performWait(storeRunnable(session, record1)); + performWait(fetchAllRunnable(session, new Record[] { record1, record2 })); + + record2.guid = Utils.generateGuid(); + performWait(storeRunnable(session, record2)); + performWait(fetchAllRunnable(session, new Record[] { record1, record2 })); + + dispose(session); + } + + public void testRawFetch() throws RemoteException { + RepositorySession session = createAndBeginSession(); + Record[] expected = new Record[] { PasswordHelpers.createPassword1(), + PasswordHelpers.createPassword2() }; + + performWait(storeRunnable(session, expected[0])); + performWait(storeRunnable(session, expected[1])); + + ContentProviderClient client = getApplicationContext().getContentResolver().acquireContentProviderClient(BrowserContract.PASSWORDS_AUTHORITY_URI); + Cursor cursor = client.query(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null, null, null); + assertEquals(2, cursor.getCount()); + cursor.moveToFirst(); + Set<String> guids = new HashSet<String>(); + while (!cursor.isAfterLast()) { + String guid = RepoUtils.getStringFromCursor(cursor, BrowserContract.Passwords.GUID); + guids.add(guid); + cursor.moveToNext(); + } + cursor.close(); + assertEquals(2, guids.size()); + assertTrue(guids.contains(expected[0].guid)); + assertTrue(guids.contains(expected[1].guid)); + dispose(session); + } + + // Helper methods. + private RepositorySession createAndBeginSession() { + return SessionTestHelper.createAndBeginSession( + getApplicationContext(), + getRepository()); + } + + private Repository getRepository() { + /** + * Override this chain in order to avoid our test code having to create two + * sessions all the time. Don't track records, so they filtering doesn't happen. + */ + return new PasswordsRepositorySession.PasswordsRepository() { + @Override + public void createSession(RepositorySessionCreationDelegate delegate, + Context context) { + PasswordsRepositorySession session; + session = new PasswordsRepositorySession(this, context) { + @Override + protected synchronized void trackGUID(String guid) { + } + }; + delegate.onSessionCreated(session); + } + }; + } + + private void wipe() { + Context context = getApplicationContext(); + context.getContentResolver().delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null); + context.getContentResolver().delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, null, null); + } + + private void storeLocalDeletedRecord(Record record, long time) { + // Wipe data-store + wipe(); + // Store record in deleted table. + ContentValues contentValues = new ContentValues(); + contentValues.put(BrowserContract.DeletedColumns.GUID, record.guid); + contentValues.put(BrowserContract.DeletedColumns.TIME_DELETED, time); + contentValues.put(BrowserContract.DeletedColumns.ID, record.androidID); + getApplicationContext().getContentResolver().insert(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, contentValues); + } + + private static void dispose(RepositorySession session) { + if (session != null) { + session.abort(); + } + } + + private static long updatePassword(String password, PasswordRecord record, long timestamp) { + record.encryptedPassword = password; + long modifiedTime = System.currentTimeMillis(); + record.timePasswordChanged = record.lastModified = modifiedTime; + return modifiedTime; + } + + private static long updatePassword(String password, PasswordRecord record) { + return updatePassword(password, record, System.currentTimeMillis()); + } + + // Runnable Helpers. + private static Runnable storeRunnable(final RepositorySession session, final Record record) { + return storeRunnable(session, record, new ExpectStoredDelegate(record.guid)); + } + + private static Runnable storeRunnable(final RepositorySession session, final Record record, final RepositorySessionStoreDelegate delegate) { + return new Runnable() { + @Override + public void run() { + session.setStoreDelegate(delegate); + try { + session.store(record); + session.storeDone(); + } catch (NoStoreDelegateException e) { + fail("NoStoreDelegateException should not occur."); + } + } + }; + } + + private static Runnable fetchAllRunnable(final RepositorySession session, final Record[] records) { + return new Runnable() { + @Override + public void run() { + session.fetchAll(new ExpectFetchDelegate(records)); + } + }; + } + + private static Runnable guidsSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) { + return new Runnable() { + @Override + public void run() { + session.guidsSince(timestamp, new ExpectGuidsSinceDelegate(expected)); + } + }; + } + + private static Runnable fetchSinceRunnable(final RepositorySession session, final long timestamp, final String[] expected) { + return new Runnable() { + @Override + public void run() { + session.fetchSince(timestamp, new ExpectFetchSinceDelegate(timestamp, expected)); + } + }; + } + + private static Runnable fetchRunnable(final RepositorySession session, final String[] guids, final Record[] expected) { + return new Runnable() { + @Override + public void run() { + try { + session.fetch(guids, new ExpectFetchDelegate(expected)); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + }; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestTopSites.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestTopSites.java new file mode 100644 index 000000000..003fc7172 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestTopSites.java @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.db; + + +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.db.BrowserContract.Combined; +import org.mozilla.gecko.db.SuggestedSites; +import org.mozilla.gecko.sync.setup.Constants; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.test.ActivityInstrumentationTestCase2; + +/** + * Exercise BrowserDB's getTopSites + * + * @author ahunt + * + */ +public class TestTopSites extends ActivityInstrumentationTestCase2<Activity> { + Context mContext; + SuggestedSites mSuggestedSites; + + public TestTopSites() { + super(Activity.class); + } + + @Override + public void setUp() { + mContext = getInstrumentation().getTargetContext(); + mSuggestedSites = new SuggestedSites(mContext); + + // By default we're using StubBrowserDB which has no suggested sites available. + BrowserDB.from(GeckoProfile.get(mContext, Constants.DEFAULT_PROFILE)).setSuggestedSites(mSuggestedSites); + } + + @Override + public void tearDown() { + BrowserDB.from(GeckoProfile.get(mContext, Constants.DEFAULT_PROFILE)).setSuggestedSites(null); + } + + public void testGetTopSites() { + final int SUGGESTED_LIMIT = 6; + final int TOTAL_LIMIT = 50; + + ContentResolver cr = mContext.getContentResolver(); + + final Uri uri = BrowserContract.TopSites.CONTENT_URI + .buildUpon() + .appendQueryParameter(BrowserContract.PARAM_PROFILE, + Constants.DEFAULT_PROFILE) + .appendQueryParameter(BrowserContract.PARAM_LIMIT, + String.valueOf(SUGGESTED_LIMIT)) + .appendQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT, + String.valueOf(TOTAL_LIMIT)) + .build(); + + final Cursor c = cr.query(uri, + new String[] { Combined._ID, + Combined.URL, + Combined.TITLE, + Combined.BOOKMARK_ID, + Combined.HISTORY_ID }, + null, + null, + null); + + int suggestedCount = 0; + try { + while (c.moveToNext()) { + int type = c.getInt(c.getColumnIndexOrThrow(BrowserContract.Bookmarks.TYPE)); + assertEquals(BrowserContract.TopSites.TYPE_SUGGESTED, type); + suggestedCount++; + } + } finally { + c.close(); + } + + Cursor suggestedSitesCursor = mSuggestedSites.get(SUGGESTED_LIMIT); + + assertEquals(suggestedSitesCursor.getCount(), suggestedCount); + + suggestedSitesCursor.close(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestAccountLoader.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestAccountLoader.java new file mode 100644 index 000000000..3009cac3e --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestAccountLoader.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package org.mozilla.gecko.background.fxa; + +import android.accounts.Account; +import android.content.Context; +import android.content.Loader; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import org.mozilla.gecko.background.sync.AndroidSyncTestCaseWithAccounts; +import org.mozilla.gecko.fxa.AccountLoader; +import org.mozilla.gecko.fxa.FirefoxAccounts; +import org.mozilla.gecko.fxa.FxAccountConstants; +import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; +import org.mozilla.gecko.fxa.login.Separated; +import org.mozilla.gecko.fxa.login.State; + +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A version of https://android.googlesource.com/platform/frameworks/base/+/c91893511dc1b9e634648406c9ae61b15476e65d/test-runner/src/android/test/LoaderTestCase.java, + * hacked to work with the v4 support library, and patched to work around + * https://code.google.com/p/android/issues/detail?id=40987. + */ +public class TestAccountLoader extends AndroidSyncTestCaseWithAccounts { + // Test account names must start with TEST_USERNAME in order to be recognized + // as test accounts and deleted in tearDown. + private static final String TEST_USERNAME = "testAccount@mozilla.com"; + private static final String TEST_ACCOUNTTYPE = FxAccountConstants.ACCOUNT_TYPE; + + private static final String TEST_SYNCKEY = "testSyncKey"; + private static final String TEST_SYNCPASSWORD = "testSyncPassword"; + + private static final String TEST_TOKEN_SERVER_URI = "testTokenServerURI"; + private static final String TEST_PROFILE_SERVER_URI = "testProfileServerURI"; + private static final String TEST_AUTH_SERVER_URI = "testAuthServerURI"; + private static final String TEST_PROFILE = "testProfile"; + + public TestAccountLoader() { + super(TEST_ACCOUNTTYPE, TEST_USERNAME); + } + + static { + // Force class loading of AsyncTask on the main thread so that it's handlers are tied to + // the main thread and responses from the worker thread get delivered on the main thread. + // The tests are run on another thread, allowing them to block waiting on a response from + // the code running on the main thread. The main thread can't block since the AsyncTask + // results come in via the event loop. + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... args) { + return null; + } + + @Override + protected void onPostExecute(Void result) { + } + }; + } + + /** + * Runs a Loader synchronously and returns the result of the load. The loader will + * be started, stopped, and destroyed by this method so it cannot be reused. + * + * @param loader The loader to run synchronously + * @return The result from the loader + */ + public <T> T getLoaderResultSynchronously(final Loader<T> loader) { + // The test thread blocks on this queue until the loader puts it's result in + final ArrayBlockingQueue<AtomicReference<T>> queue = new ArrayBlockingQueue<AtomicReference<T>>(1); + + // This callback runs on the "main" thread and unblocks the test thread + // when it puts the result into the blocking queue + final Loader.OnLoadCompleteListener<T> listener = new Loader.OnLoadCompleteListener<T>() { + @Override + public void onLoadComplete(Loader<T> completedLoader, T data) { + // Shut the loader down + completedLoader.unregisterListener(this); + completedLoader.stopLoading(); + completedLoader.reset(); + // Store the result, unblocking the test thread + queue.add(new AtomicReference<T>(data)); + } + }; + + // This handler runs on the "main" thread of the process since AsyncTask + // is documented as needing to run on the main thread and many Loaders use + // AsyncTask + final Handler mainThreadHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + loader.registerListener(0, listener); + loader.startLoading(); + } + }; + + // Ask the main thread to start the loading process + mainThreadHandler.sendEmptyMessage(0); + + // Block on the queue waiting for the result of the load to be inserted + T result; + while (true) { + try { + result = queue.take().get(); + break; + } catch (InterruptedException e) { + throw new RuntimeException("waiting thread interrupted", e); + } + } + return result; + } + + public void testInitialLoad() throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException { + // This is tricky. We can't mock the AccountManager easily -- see + // https://groups.google.com/d/msg/android-mock/VXyzvKTMUGs/Y26wVPrl50sJ -- + // and we don't want to delete any existing accounts on device. So our test + // needs to be adaptive (and therefore a little race-prone). + + final Context context = getApplicationContext(); + final AccountLoader loader = new AccountLoader(context); + + final boolean firefoxAccountsExist = FirefoxAccounts.firefoxAccountsExist(context); + + if (firefoxAccountsExist) { + assertFirefoxAccount(getLoaderResultSynchronously((Loader<Account>) loader)); + return; + } + + // This account will get cleaned up in tearDown. + final State state = new Separated(TEST_USERNAME, "uid", false); // State choice is arbitrary. + final AndroidFxAccount account = AndroidFxAccount.addAndroidAccount(context, + TEST_USERNAME, TEST_PROFILE, TEST_AUTH_SERVER_URI, TEST_TOKEN_SERVER_URI, TEST_PROFILE_SERVER_URI, + state, AndroidSyncTestCaseWithAccounts.TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED); + assertNotNull(account); + assertFirefoxAccount(getLoaderResultSynchronously((Loader<Account>) loader)); + } + + protected void assertFirefoxAccount(Account account) { + assertNotNull(account); + assertEquals(FxAccountConstants.ACCOUNT_TYPE, account.type); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java new file mode 100644 index 000000000..18fb58a97 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.fxa; + +import java.security.GeneralSecurityException; + +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.browserid.BrowserIDKeyPair; +import org.mozilla.gecko.browserid.DSACryptoImplementation; +import org.mozilla.gecko.browserid.JSONWebTokenUtils; +import org.mozilla.gecko.browserid.RSACryptoImplementation; +import org.mozilla.gecko.browserid.SigningPrivateKey; +import org.mozilla.gecko.browserid.VerifyingPublicKey; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.Utils; + +public class TestBrowserIDKeyPairGeneration extends AndroidSyncTestCase { + public void doTestEncodeDecode(BrowserIDKeyPair keyPair) throws Exception { + SigningPrivateKey privateKey = keyPair.getPrivate(); + VerifyingPublicKey publicKey = keyPair.getPublic(); + + ExtendedJSONObject o = new ExtendedJSONObject(); + o.put("key", Utils.generateGuid()); + + String token = JSONWebTokenUtils.encode(o.toJSONString(), privateKey); + assertNotNull(token); + + String payload = JSONWebTokenUtils.decode(token, publicKey); + assertEquals(o.toJSONString(), payload); + + try { + JSONWebTokenUtils.decode(token + "x", publicKey); + fail("Expected exception."); + } catch (GeneralSecurityException e) { + // Do nothing. + } + } + + public void testEncodeDecodeSuccessRSA() throws Exception { + doTestEncodeDecode(RSACryptoImplementation.generateKeyPair(1024)); + doTestEncodeDecode(RSACryptoImplementation.generateKeyPair(2048)); + } + + public void testEncodeDecodeSuccessDSA() throws Exception { + doTestEncodeDecode(DSACryptoImplementation.generateKeyPair(512)); + doTestEncodeDecode(DSACryptoImplementation.generateKeyPair(1024)); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java new file mode 100644 index 000000000..d50bd47e0 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.fxa.authenticator; + +import org.mozilla.gecko.background.sync.AndroidSyncTestCaseWithAccounts; +import org.mozilla.gecko.fxa.FxAccountConstants; +import org.mozilla.gecko.fxa.authenticator.AccountPickler; +import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; +import org.mozilla.gecko.fxa.login.Separated; +import org.mozilla.gecko.fxa.login.State; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.Utils; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.test.RenamingDelegatingContext; + +public class TestAccountPickler extends AndroidSyncTestCaseWithAccounts { + private static final String TEST_TOKEN_SERVER_URI = "tokenServerURI"; + private static final String TEST_PROFILE_SERVER_URI = "profileServerURI"; + private static final String TEST_AUTH_SERVER_URI = "serverURI"; + private static final String TEST_PROFILE = "profile"; + private final static String FILENAME_PREFIX = "TestAccountPickler-"; + private final static String PICKLE_FILENAME = "pickle"; + + private final static String TEST_ACCOUNTTYPE = FxAccountConstants.ACCOUNT_TYPE; + + // Test account names must start with TEST_USERNAME in order to be recognized + // as test accounts and deleted in tearDown. + public static final String TEST_USERNAME = "testFirefoxAccount@mozilla.com"; + + public Account account; + public RenamingDelegatingContext context; + + public TestAccountPickler() { + super(TEST_ACCOUNTTYPE, TEST_USERNAME); + } + + @Override + public void setUp() { + super.setUp(); + this.account = null; + // Randomize the filename prefix in case we don't clean up correctly. + this.context = new RenamingDelegatingContext(getApplicationContext(), FILENAME_PREFIX + + Math.random() * 1000001 + "-"); + this.accountManager = AccountManager.get(context); + } + + @Override + public void tearDown() { + super.tearDown(); + this.context.deleteFile(PICKLE_FILENAME); + } + + public AndroidFxAccount addTestAccount() throws Exception { + final State state = new Separated(TEST_USERNAME, "uid", false); // State choice is arbitrary. + final AndroidFxAccount account = AndroidFxAccount.addAndroidAccount(context, TEST_USERNAME, + TEST_PROFILE, TEST_AUTH_SERVER_URI, TEST_TOKEN_SERVER_URI, TEST_PROFILE_SERVER_URI, state, + AndroidSyncTestCaseWithAccounts.TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED); + assertNotNull(account); + assertNotNull(account.getProfile()); + assertTrue(testAccountsExist()); // Sanity check. + this.account = account.getAndroidAccount(); // To remove in tearDown() if we throw. + return account; + } + + public void testPickle() throws Exception { + final AndroidFxAccount account = addTestAccount(); + + final long now = System.currentTimeMillis(); + final ExtendedJSONObject o = AccountPickler.toJSON(account, now); + assertNotNull(o.toJSONString()); + + assertEquals(3, o.getLong(AccountPickler.KEY_PICKLE_VERSION).longValue()); + assertTrue(o.getLong(AccountPickler.KEY_PICKLE_TIMESTAMP).longValue() < System.currentTimeMillis()); + + assertEquals(AndroidFxAccount.CURRENT_ACCOUNT_VERSION, o.getIntegerSafely(AccountPickler.KEY_ACCOUNT_VERSION).intValue()); + assertEquals(FxAccountConstants.ACCOUNT_TYPE, o.getString(AccountPickler.KEY_ACCOUNT_TYPE)); + + assertEquals(TEST_USERNAME, o.getString(AccountPickler.KEY_EMAIL)); + assertEquals(TEST_PROFILE, o.getString(AccountPickler.KEY_PROFILE)); + assertEquals(TEST_AUTH_SERVER_URI, o.getString(AccountPickler.KEY_IDP_SERVER_URI)); + assertEquals(TEST_TOKEN_SERVER_URI, o.getString(AccountPickler.KEY_TOKEN_SERVER_URI)); + assertEquals(TEST_PROFILE_SERVER_URI, o.getString(AccountPickler.KEY_PROFILE_SERVER_URI)); + + assertNotNull(o.getObject(AccountPickler.KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP)); + assertNotNull(o.get(AccountPickler.KEY_BUNDLE)); + } + + public void testPickleAndUnpickle() throws Exception { + final AndroidFxAccount inputAccount = addTestAccount(); + + AccountPickler.pickle(inputAccount, PICKLE_FILENAME); + final ExtendedJSONObject inputJSON = AccountPickler.toJSON(inputAccount, 0); + final State inputState = inputAccount.getState(); + assertNotNull(inputJSON); + assertNotNull(inputState); + + // unpickle adds an account to the AccountManager so delete it first. + deleteTestAccounts(); + assertFalse(testAccountsExist()); + + final AndroidFxAccount unpickledAccount = AccountPickler.unpickle(context, PICKLE_FILENAME); + assertNotNull(unpickledAccount); + final ExtendedJSONObject unpickledJSON = AccountPickler.toJSON(unpickledAccount, 0); + final State unpickledState = unpickledAccount.getState(); + assertNotNull(unpickledJSON); + assertNotNull(unpickledState); + + assertEquals(inputJSON, unpickledJSON); + assertStateEquals(inputState, unpickledState); + } + + public void testDeletePickle() throws Exception { + final AndroidFxAccount account = addTestAccount(); + AccountPickler.pickle(account, PICKLE_FILENAME); + + final String s = Utils.readFile(context, PICKLE_FILENAME); + assertNotNull(s); + assertTrue(s.length() > 0); + + AccountPickler.deletePickle(context, PICKLE_FILENAME); + assertFileNotPresent(context, PICKLE_FILENAME); + } + + private void assertStateEquals(final State expected, final State actual) throws Exception { + // TODO: Write and use State.equals. Thus, this is only thorough for the State base class. + assertEquals(expected.getStateLabel(), actual.getStateLabel()); + assertEquals(expected.email, actual.email); + assertEquals(expected.uid, actual.uid); + assertEquals(expected.verified, actual.verified); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java new file mode 100644 index 000000000..5cdbe44f4 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.helpers; + +import junit.framework.AssertionFailedError; + +import org.mozilla.gecko.background.testhelpers.WaitHelper; + +import android.app.Activity; +import android.content.Context; +import android.test.ActivityInstrumentationTestCase2; + +/** + * AndroidSyncTestCase provides helper methods for testing. + */ +public class AndroidSyncTestCase extends ActivityInstrumentationTestCase2<Activity> { + protected static String LOG_TAG = "AndroidSyncTestCase"; + + public AndroidSyncTestCase() { + super(Activity.class); + WaitHelper.resetTestWaiter(); + } + + public Context getApplicationContext() { + return this.getInstrumentation().getTargetContext(); + } + + public static void performWait(Runnable runnable) { + try { + WaitHelper.getTestWaiter().performWait(runnable); + } catch (WaitHelper.InnerError e) { + AssertionFailedError inner = new AssertionFailedError("Caught error in performWait"); + inner.initCause(e.innerError); + throw inner; + } + } + + public static void performNotify() { + WaitHelper.getTestWaiter().performNotify(); + } + + public static void performNotify(Throwable e) { + WaitHelper.getTestWaiter().performNotify(e); + } + + public static void performNotify(String reason, Throwable e) { + AssertionFailedError er = new AssertionFailedError(reason + ": " + e.getMessage()); + er.initCause(e); + WaitHelper.getTestWaiter().performNotify(er); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBHelpers.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBHelpers.java new file mode 100644 index 000000000..c37f1e8bd --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBHelpers.java @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.helpers; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import junit.framework.Assert; + +public class DBHelpers { + + /* + * Works for strings and int-ish values. + */ + public static void assertCursorContains(Object[][] expected, Cursor actual) { + Assert.assertEquals(expected.length, actual.getCount()); + int i = 0, j = 0; + Object[] row; + + do { + row = expected[i]; + for (j = 0; j < row.length; ++j) { + Object atIndex = row[j]; + if (atIndex == null) { + continue; + } + if (atIndex instanceof String) { + Assert.assertEquals(atIndex, actual.getString(j)); + } else { + Assert.assertEquals(atIndex, actual.getInt(j)); + } + } + ++i; + } while (actual.moveToPosition(i)); + } + + public static int getRowCount(SQLiteDatabase db, String table) { + return getRowCount(db, table, null, null); + } + + public static int getRowCount(SQLiteDatabase db, String table, String selection, String[] selectionArgs) { + final Cursor c = db.query(table, null, selection, selectionArgs, null, null, null); + try { + return c.getCount(); + } finally { + c.close(); + } + } + + /** + * Returns an ID that is non-existent in the given sqlite table. Assumes that a column named + * "id" exists. + */ + public static int getNonExistentID(SQLiteDatabase db, String table) { + // XXX: We should use selectionArgs to concatenate table, but sqlite throws a syntax error on + // "?" because it wants to ensure id is a valid column in table. + final Cursor c = db.rawQuery("SELECT MAX(id) + 1 FROM " + table, null); + try { + if (!c.moveToNext()) { + return 0; + } + return c.getInt(0); + } finally { + c.close(); + } + } + + /** + * Returns an ID that exists in the given sqlite table. Assumes that a column named * "id" + * exists. + */ + public static long getExistentID(SQLiteDatabase db, String table) { + final Cursor c = db.query(table, new String[] {"id"}, null, null, null, null, null, "1"); + try { + if (!c.moveToNext()) { + throw new IllegalStateException("Given table does not contain any entries."); + } + return c.getInt(0); + } finally { + c.close(); + } + } + +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java new file mode 100644 index 000000000..ccfeb8d63 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.helpers; + +import java.io.File; + +import android.content.ContentProvider; +import android.content.Context; +import android.test.AndroidTestCase; +import android.test.mock.MockContentResolver; + +/** + * Because ProviderTestCase2 is unable to handle custom DB paths. + */ +public abstract class DBProviderTestCase<T extends ContentProvider> extends + AndroidTestCase { + + Class<T> providerClass; + String providerAuthority; + + protected File fakeProfileDirectory; + private MockContentResolver resolver; + private T provider; + + public DBProviderTestCase(Class<T> providerClass, String providerAuthority) { + this.providerClass = providerClass; + this.providerAuthority = providerAuthority; + } + + public T getProvider() { + return provider; + } + + public MockContentResolver getMockContentResolver() { + return resolver; + } + + protected String getCacheSuffix() { + return this.getClass().getName() + "-" + System.currentTimeMillis(); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + File cache = getContext().getCacheDir(); + fakeProfileDirectory = new File(cache.getAbsolutePath() + getCacheSuffix()); + System.out.println("Test: Creating profile directory " + fakeProfileDirectory.getAbsolutePath()); + if (!fakeProfileDirectory.mkdir()) { + throw new IllegalStateException("Could not create temporary directory."); + } + + final Context context = getContext(); + assertNotNull(context); + resolver = new MockContentResolver(); + provider = providerClass.newInstance(); + provider.attachInfo(context, null); + assertNotNull(provider); + resolver.addProvider(providerAuthority, getProvider()); + } + + @Override + protected void tearDown() throws Exception { + // We don't check return values. + System.out.println("Test: Cleaning up " + fakeProfileDirectory.getAbsolutePath()); + for (File child : fakeProfileDirectory.listFiles()) { + child.delete(); + } + fakeProfileDirectory.delete(); + super.tearDown(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java new file mode 100644 index 000000000..d24f28491 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java @@ -0,0 +1,175 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.nativecode.test; + +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import junit.framework.TestCase; + +import org.mozilla.gecko.background.nativecode.NativeCrypto; +import org.mozilla.gecko.sync.Utils; + +/* + * Tests the Java wrapper over native implementations of crypto code. Test vectors from: + * * PBKDF2SHA256: + * - <https://github.com/ircmaxell/PHP-PasswordLib/blob/master/test/Data/Vectors/pbkdf2-draft-josefsson-sha256.test-vectors> + * - <https://gitorious.org/scrypt/nettle-scrypt/blobs/37c0d5288e991604fe33dba2f1724986a8dddf56/testsuite/pbkdf2-test.c> + * * SHA-1: + * - <http://oauth.googlecode.com/svn/code/c/liboauth/src/sha1.c> + */ +public class TestNativeCrypto extends TestCase { + + public final void testPBKDF2SHA256A() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "password"; + String s = "salt"; + int dkLen = 32; + + checkPBKDF2SHA256(p, s, 1, dkLen, "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b"); + checkPBKDF2SHA256(p, s, 4096, dkLen, "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a"); + } + + public final void testPBKDF2SHA256B() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "passwordPASSWORDpassword"; + String s = "saltSALTsaltSALTsaltSALTsaltSALTsalt"; + int dkLen = 40; + + checkPBKDF2SHA256(p, s, 4096, dkLen, "348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c4e2a1fb8dd53e1c635518c7dac47e9"); + } + + public final void testPBKDF2SHA256scryptA() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "passwd"; + String s = "salt"; + int dkLen = 64; + + checkPBKDF2SHA256(p, s, 1, dkLen, "55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783"); + } + + public final void testPBKDF2SHA256scryptB() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "Password"; + String s = "NaCl"; + int dkLen = 64; + + checkPBKDF2SHA256(p, s, 80000, dkLen, "4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d"); + } + + public final void testPBKDF2SHA256C() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "pass\0word"; + String s = "sa\0lt"; + int dkLen = 16; + + checkPBKDF2SHA256(p, s, 4096, dkLen, "89b69d0516f829893c696226650a8687"); + } + + /* + // This test takes two or three minutes to run, so we don't. + public final void testPBKDF2SHA256D() throws UnsupportedEncodingException, GeneralSecurityException { + String p = "password"; + String s = "salt"; + int dkLen = 32; + + checkPBKDF2SHA256(p, s, 16777216, dkLen, "cf81c66fe8cfc04d1f31ecb65dab4089f7f179e89b3b0bcb17ad10e3ac6eba46"); + } + */ + + public final void testTimePBKDF2SHA256() throws UnsupportedEncodingException, GeneralSecurityException { + checkPBKDF2SHA256("password", "salt", 80000, 32, null); + } + + public final void testPBKDF2SHA256InvalidLenArg() throws UnsupportedEncodingException, GeneralSecurityException { + final String p = "password"; + final String s = "salt"; + final int c = 1; + final int dkLen = -1; // Should always be positive. + + try { + NativeCrypto.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen); + fail("Expected sha256 to throw with negative dkLen argument."); + } catch (IllegalArgumentException e) { } // Expected. + } + + public final void testSHA1() throws UnsupportedEncodingException { + final String[] inputs = new String[] { + "abc", + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "" // To be filled in below. + }; + final String baseStr = "01234567"; + final int repetitions = 80; + final StringBuilder builder = new StringBuilder(baseStr.length() * repetitions); + for (int i = 0; i < 80; ++i) { + builder.append(baseStr); + } + inputs[2] = builder.toString(); + + final String[] expecteds = new String[] { + "a9993e364706816aba3e25717850c26c9cd0d89d", + "84983e441c3bd26ebaae4aa1f95129e5e54670f1", + "dea356a2cddd90c7a7ecedc5ebb563934f460452" + }; + + for (int i = 0; i < inputs.length; ++i) { + final byte[] input = inputs[i].getBytes("US-ASCII"); + final String expected = expecteds[i]; + + final byte[] actual = NativeCrypto.sha1(input); + assertNotNull("Hashed value is non-null", actual); + assertExpectedBytes(expected, actual); + } + } + + /** + * Test to ensure the output of our SHA1 algo is the same as MessageDigest's. This is important + * because we intend to replace MessageDigest in FHR with this SHA-1 algo (bug 959652). + */ + public final void testSHA1AgainstMessageDigest() throws UnsupportedEncodingException, + NoSuchAlgorithmException { + final String[] inputs = { + "password", + "saranghae", + "aoeusnthaoeusnthaoeusnth \0 12345098765432109876_!" + }; + + final MessageDigest digest = MessageDigest.getInstance("SHA-1"); + for (final String input : inputs) { + final byte[] inputBytes = input.getBytes("US-ASCII"); + + final byte[] mdBytes = digest.digest(inputBytes); + final byte[] ourBytes = NativeCrypto.sha1(inputBytes); + assertTrue("MessageDigest hash is the same as NativeCrypto SHA-1 hash", + Arrays.equals(ourBytes, mdBytes)); + } + } + + private void checkPBKDF2SHA256(String p, String s, int c, int dkLen, + final String expectedStr) + throws GeneralSecurityException, UnsupportedEncodingException { + long start = System.currentTimeMillis(); + byte[] key = NativeCrypto.pbkdf2SHA256(p.getBytes("US-ASCII"), s.getBytes("US-ASCII"), c, dkLen); + assertNotNull(key); + + long end = System.currentTimeMillis(); + + System.err.println("SHA-256 " + c + " took " + (end - start) + "ms"); + if (expectedStr == null) { + return; + } + + assertEquals(dkLen, Utils.hex2Byte(expectedStr).length); + assertExpectedBytes(expectedStr, key); + } + + private void assertExpectedBytes(final String expectedStr, byte[] key) { + assertEquals(expectedStr, Utils.byte2Hex(key)); + byte[] expected = Utils.hex2Byte(expectedStr); + + assertEquals(expected.length, key.length); + for (int i = 0; i < key.length; i++) { + assertEquals(expected[i], key[i]); + } + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java new file mode 100644 index 000000000..e0e4e8bb1 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.content.Context; +import android.test.InstrumentationTestCase; + +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class AndroidSyncTestCaseWithAccounts extends AndroidSyncTestCase { + public final String testAccountType; + public final String testAccountPrefix; + + protected Context context; + protected AccountManager accountManager; + protected int numAccounts; + + public static final Map<String, Boolean> TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED; + static { + final Map<String, Boolean> m = new HashMap<String, Boolean>(); + for (String authority : AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP.keySet()) { + m.put(authority, false); + } + TEST_SYNC_AUTOMATICALLY_MAP_WITH_ALL_AUTHORITIES_DISABLED = m; + } + + public AndroidSyncTestCaseWithAccounts(String accountType, String accountPrefix) { + super(); + this.testAccountType = accountType; + this.testAccountPrefix = accountPrefix; + } + + @Override + public void setUp() { + context = getApplicationContext(); + accountManager = AccountManager.get(context); + deleteTestAccounts(); // Always start with no test accounts. + numAccounts = accountManager.getAccountsByType(testAccountType).length; + } + + public List<Account> getTestAccounts() { + final List<Account> testAccounts = new ArrayList<Account>(); + + final Account[] accounts = accountManager.getAccountsByType(testAccountType); + for (Account account : accounts) { + if (account.name.startsWith(testAccountPrefix)) { + testAccounts.add(account); + } + } + + return testAccounts; + } + + public static void deleteAccount(final InstrumentationTestCase test, final AccountManager accountManager, final Account account) { + performWait(new Runnable() { + @Override + public void run() { + try { + test.runTestOnUiThread(new Runnable() { + final AccountManagerCallback<Boolean> callback = new AccountManagerCallback<Boolean>() { + @Override + public void run(AccountManagerFuture<Boolean> future) { + try { + future.getResult(5L, TimeUnit.SECONDS); + } catch (Exception e) { + } + performNotify(); + } + }; + + @Override + public void run() { + accountManager.removeAccount(account, callback, null); + } + }); + } catch (Throwable e) { + performNotify(e); + } + } + }); + } + + public void deleteTestAccounts() { + for (Account account : getTestAccounts()) { + deleteAccount(this, accountManager, account); + } + } + + public boolean testAccountsExist() { + // Note that we don't use FirefoxAccounts.firefoxAccountsExist because it unpickles. + return !getTestAccounts().isEmpty(); + } + + @Override + public void tearDown() { + deleteTestAccounts(); + assertEquals(numAccounts, accountManager.getAccountsByType(testAccountType).length); + } + + public static void assertFileNotPresent(final Context context, final String filename) throws Exception { + // Verify file is not present. + FileInputStream fis = null; + try { + fis = context.openFileInput(filename); + fail("Should get FileNotFoundException."); + } catch (FileNotFoundException e) { + // Do nothing; file should not exist. + } finally { + if (fis != null) { + fis.close(); + } + } + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java new file mode 100644 index 000000000..39e24b4d4 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync; + +import org.json.simple.JSONArray; +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback; +import org.mozilla.gecko.background.testhelpers.MockClientsDataDelegate; +import org.mozilla.gecko.background.testhelpers.MockSharedPreferences; +import org.mozilla.gecko.sync.GlobalSession; +import org.mozilla.gecko.sync.SyncConfiguration; +import org.mozilla.gecko.sync.crypto.KeyBundle; +import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; +import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider; +import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; +import org.mozilla.gecko.sync.repositories.domain.ClientRecord; +import org.mozilla.gecko.sync.stage.SyncClientsEngineStage; + +import android.content.Context; +import android.content.SharedPreferences; + +public class TestClientsStage extends AndroidSyncTestCase { + private static final String TEST_USERNAME = "johndoe"; + private static final String TEST_PASSWORD = "password"; + private static final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea"; + + @Override + public void setUp() { + ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(getApplicationContext()); + db.wipeDB(); + db.close(); + } + + public void testWipeClearsClients() throws Exception { + + // Wiping clients is equivalent to a reset and dropping all local stored client records. + // Resetting is defined as being the same as for other engines -- discard local + // and remote timestamps, tracked failed records, and tracked records to fetch. + + final Context context = getApplicationContext(); + final ClientsDatabaseAccessor dataAccessor = new ClientsDatabaseAccessor(context); + final GlobalSessionCallback callback = new DefaultGlobalSessionCallback(); + final ClientsDataDelegate delegate = new MockClientsDataDelegate(); + + final KeyBundle keyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY); + final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD); + final SharedPreferences prefs = new MockSharedPreferences(); + final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, authHeaderProvider, prefs); + config.syncKeyBundle = keyBundle; + GlobalSession session = new GlobalSession(config, callback, context, delegate); + + SyncClientsEngineStage stage = new SyncClientsEngineStage() { + + @Override + public synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() { + if (db == null) { + db = dataAccessor; + } + return db; + } + }; + + final String guid = "clientabcdef"; + long lastModified = System.currentTimeMillis(); + ClientRecord record = new ClientRecord(guid, "clients", lastModified , false); + record.name = "John's Phone"; + record.type = "mobile"; + record.device = "Some Device"; + record.os = "iOS"; + record.commands = new JSONArray(); + + dataAccessor.store(record); + assertEquals(1, dataAccessor.clientsCount()); + + final ClientRecord stored = dataAccessor.fetchAllClients().get(guid); + assertNotNull(stored); + assertEquals("John's Phone", stored.name); + assertEquals("mobile", stored.type); + assertEquals("Some Device", stored.device); + assertEquals("iOS", stored.os); + + stage.wipeLocal(session); + + try { + assertEquals(0, dataAccessor.clientsCount()); + assertEquals(0L, session.config.getPersistedServerClientRecordTimestamp()); + assertEquals(0, session.getClientsDelegate().getClientsCount()); + } finally { + dataAccessor.close(); + } + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java new file mode 100644 index 000000000..52af2ad01 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java @@ -0,0 +1,198 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync; + +import android.content.SharedPreferences; + +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.background.testhelpers.BaseMockServerSyncStage; +import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback; +import org.mozilla.gecko.background.testhelpers.MockRecord; +import org.mozilla.gecko.background.testhelpers.MockSharedPreferences; +import org.mozilla.gecko.background.testhelpers.WBORepository; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.EngineSettings; +import org.mozilla.gecko.sync.GlobalSession; +import org.mozilla.gecko.sync.MetaGlobalException; +import org.mozilla.gecko.sync.SyncConfiguration; +import org.mozilla.gecko.sync.SynchronizerConfiguration; +import org.mozilla.gecko.sync.crypto.KeyBundle; +import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider; +import org.mozilla.gecko.sync.repositories.domain.Record; +import org.mozilla.gecko.sync.stage.NoSuchStageException; +import org.mozilla.gecko.sync.synchronizer.Synchronizer; + +/** + * Test the on-device side effects of reset operations on a stage. + * + * See also "TestResetCommands" in the unit test suite. + */ +public class TestResetting extends AndroidSyncTestCase { + private static final String TEST_USERNAME = "johndoe"; + private static final String TEST_PASSWORD = "password"; + private static final String TEST_SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea"; + + @Override + public void setUp() { + assertTrue(WaitHelper.getTestWaiter().isIdle()); + } + + /** + * Set up a mock stage that synchronizes two mock repositories. Apply various + * reset/sync/wipe permutations and check state. + */ + public void testResetAndWipeStage() throws Exception { + + final long startTime = System.currentTimeMillis(); + final GlobalSessionCallback callback = createGlobalSessionCallback(); + final GlobalSession session = createDefaultGlobalSession(callback); + + final ExecutableMockServerSyncStage stage = new ExecutableMockServerSyncStage() { + @Override + public void onSynchronized(Synchronizer synchronizer) { + try { + assertTrue(startTime <= synchronizer.bundleA.getTimestamp()); + assertTrue(startTime <= synchronizer.bundleB.getTimestamp()); + + // Call up to allow the usual persistence etc. to happen. + super.onSynchronized(synchronizer); + } catch (Throwable e) { + performNotify(e); + return; + } + performNotify(); + } + }; + + final boolean bumpTimestamps = true; + WBORepository local = new WBORepository(bumpTimestamps); + WBORepository remote = new WBORepository(bumpTimestamps); + + stage.name = "mock"; + stage.collection = "mock"; + stage.local = local; + stage.remote = remote; + + stage.executeSynchronously(session); + + // Verify the persisted values. + assertConfigTimestampsGreaterThan(stage.leakConfig(), startTime, startTime); + + // Reset. + stage.resetLocal(session); + + // Verify that they're gone. + assertConfigTimestampsEqual(stage.leakConfig(), 0, 0); + + // Now sync data, ensure that timestamps come back. + final long afterReset = System.currentTimeMillis(); + final String recordGUID = "abcdefghijkl"; + local.wbos.put(recordGUID, new MockRecord(recordGUID, "mock", startTime, false)); + + // Sync again with data and verify timestamps and data. + stage.executeSynchronously(session); + + assertConfigTimestampsGreaterThan(stage.leakConfig(), afterReset, afterReset); + assertEquals(1, remote.wbos.size()); + assertEquals(1, local.wbos.size()); + + Record remoteRecord = remote.wbos.get(recordGUID); + assertNotNull(remoteRecord); + assertNotNull(local.wbos.get(recordGUID)); + assertEquals(recordGUID, remoteRecord.guid); + assertTrue(afterReset <= remoteRecord.lastModified); + + // Reset doesn't clear data. + stage.resetLocal(session); + assertConfigTimestampsEqual(stage.leakConfig(), 0, 0); + assertEquals(1, remote.wbos.size()); + assertEquals(1, local.wbos.size()); + remoteRecord = remote.wbos.get(recordGUID); + assertNotNull(remoteRecord); + assertNotNull(local.wbos.get(recordGUID)); + + // Wipe does. Recover from reset... + final long beforeWipe = System.currentTimeMillis(); + stage.executeSynchronously(session); + assertEquals(1, remote.wbos.size()); + assertEquals(1, local.wbos.size()); + assertConfigTimestampsGreaterThan(stage.leakConfig(), beforeWipe, beforeWipe); + + // ... then wipe. + stage.wipeLocal(session); + assertConfigTimestampsEqual(stage.leakConfig(), 0, 0); + assertEquals(1, remote.wbos.size()); // We don't wipe the server. + assertEquals(0, local.wbos.size()); // We do wipe local. + } + + /** + * A stage that joins two Repositories with no wrapping. + */ + public class ExecutableMockServerSyncStage extends BaseMockServerSyncStage { + /** + * Run this stage synchronously. + */ + public void executeSynchronously(final GlobalSession session) { + final BaseMockServerSyncStage self = this; + performWait(new Runnable() { + @Override + public void run() { + try { + self.execute(session); + } catch (NoSuchStageException e) { + performNotify(e); + } + } + }); + } + } + + private GlobalSession createDefaultGlobalSession(final GlobalSessionCallback callback) throws Exception { + final KeyBundle keyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY); + final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD); + final SharedPreferences prefs = new MockSharedPreferences(); + final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, authHeaderProvider, prefs); + config.syncKeyBundle = keyBundle; + return new GlobalSession(config, callback, getApplicationContext(), null) { + @Override + public boolean isEngineRemotelyEnabled(String engineName, + EngineSettings engineSettings) + throws MetaGlobalException { + return true; + } + + @Override + public void advance() { + // So we don't proceed and run other stages. + } + }; + } + + private static GlobalSessionCallback createGlobalSessionCallback() { + return new DefaultGlobalSessionCallback() { + + @Override + public void handleAborted(GlobalSession globalSession, String reason) { + performNotify(new Exception("Aborted")); + } + + @Override + public void handleError(GlobalSession globalSession, Exception ex) { + performNotify(ex); + } + }; + } + + private static void assertConfigTimestampsGreaterThan(SynchronizerConfiguration config, long local, long remote) { + assertTrue(local <= config.localBundle.getTimestamp()); + assertTrue(remote <= config.remoteBundle.getTimestamp()); + } + + private static void assertConfigTimestampsEqual(SynchronizerConfiguration config, long local, long remote) { + assertEquals(local, config.localBundle.getTimestamp()); + assertEquals(remote, config.remoteBundle.getTimestamp()); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java new file mode 100644 index 000000000..bac6c7f49 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java @@ -0,0 +1,377 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import junit.framework.AssertionFailedError; + +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessBeginDelegate; +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessCreationDelegate; +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFetchDelegate; +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessFinishDelegate; +import org.mozilla.gecko.background.sync.helpers.SimpleSuccessStoreDelegate; +import org.mozilla.gecko.background.testhelpers.WBORepository; +import org.mozilla.gecko.sync.CryptoRecord; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.repositories.InactiveSessionException; +import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; +import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; +import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; +import org.mozilla.gecko.sync.repositories.domain.Record; +import org.mozilla.gecko.sync.synchronizer.Synchronizer; +import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate; + +import android.content.Context; + +public class TestStoreTracking extends AndroidSyncTestCase { + public void assertEq(Object expected, Object actual) { + try { + assertEquals(expected, actual); + } catch (AssertionFailedError e) { + performNotify(e); + } + } + + public class TrackingWBORepository extends WBORepository { + @Override + public synchronized boolean shouldTrack() { + return true; + } + } + + public void doTestStoreRetrieveByGUID(final WBORepository repository, + final RepositorySession session, + final String expectedGUID, + final Record record) { + + final SimpleSuccessStoreDelegate storeDelegate = new SimpleSuccessStoreDelegate() { + + @Override + public void onRecordStoreSucceeded(String guid) { + Logger.debug(getName(), "Stored " + guid); + assertEq(expectedGUID, guid); + } + + @Override + public void onStoreCompleted(long storeEnd) { + Logger.debug(getName(), "Store completed at " + storeEnd + "."); + try { + session.fetch(new String[] { expectedGUID }, new SimpleSuccessFetchDelegate() { + @Override + public void onFetchedRecord(Record record) { + Logger.debug(getName(), "Hurrah! Fetched record " + record.guid); + assertEq(expectedGUID, record.guid); + } + + @Override + public void onFetchCompleted(final long fetchEnd) { + Logger.debug(getName(), "Fetch completed at " + fetchEnd + "."); + + // But fetching by time returns nothing. + session.fetchSince(0, new SimpleSuccessFetchDelegate() { + private AtomicBoolean fetched = new AtomicBoolean(false); + + @Override + public void onFetchedRecord(Record record) { + Logger.debug(getName(), "Fetched record " + record.guid); + fetched.set(true); + performNotify(new AssertionFailedError("Should have fetched no record!")); + } + + @Override + public void onFetchCompleted(final long fetchEnd) { + if (fetched.get()) { + Logger.debug(getName(), "Not finishing session: record retrieved."); + return; + } + try { + session.finish(new SimpleSuccessFinishDelegate() { + @Override + public void onFinishSucceeded(RepositorySession session, + RepositorySessionBundle bundle) { + performNotify(); + } + }); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + }); + } + }); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + }; + + session.setStoreDelegate(storeDelegate); + try { + Logger.debug(getName(), "Storing..."); + session.store(record); + session.storeDone(); + } catch (NoStoreDelegateException e) { + // Should not happen. + } + } + + private void doTestNewSessionRetrieveByTime(final WBORepository repository, + final String expectedGUID) { + final SimpleSuccessCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() { + @Override + public void onSessionCreated(final RepositorySession session) { + Logger.debug(getName(), "Session created."); + try { + session.begin(new SimpleSuccessBeginDelegate() { + @Override + public void onBeginSucceeded(final RepositorySession session) { + // Now we get a result. + session.fetchSince(0, new SimpleSuccessFetchDelegate() { + + @Override + public void onFetchedRecord(Record record) { + assertEq(expectedGUID, record.guid); + } + + @Override + public void onFetchCompleted(long end) { + try { + session.finish(new SimpleSuccessFinishDelegate() { + @Override + public void onFinishSucceeded(RepositorySession session, + RepositorySessionBundle bundle) { + // Hooray! + performNotify(); + } + }); + } catch (InactiveSessionException e) { + performNotify(e); + } + } + }); + } + }); + } catch (InvalidSessionTransitionException e) { + performNotify(e); + } + } + }; + Runnable create = new Runnable() { + @Override + public void run() { + repository.createSession(createDelegate, getApplicationContext()); + } + }; + + performWait(create); + } + + /** + * Store a record in one session. Verify that fetching by GUID returns + * the record. Verify that fetching by timestamp fails to return records. + * Start a new session. Verify that fetching by timestamp returns the + * stored record. + * + * Invokes doTestStoreRetrieveByGUID, doTestNewSessionRetrieveByTime. + */ + public void testStoreRetrieveByGUID() { + Logger.debug(getName(), "Started."); + final WBORepository r = new TrackingWBORepository(); + final long now = System.currentTimeMillis(); + final String expectedGUID = "abcdefghijkl"; + final Record record = new BookmarkRecord(expectedGUID, "bookmarks", now , false); + + final RepositorySessionCreationDelegate createDelegate = new SimpleSuccessCreationDelegate() { + @Override + public void onSessionCreated(RepositorySession session) { + Logger.debug(getName(), "Session created: " + session); + try { + session.begin(new SimpleSuccessBeginDelegate() { + @Override + public void onBeginSucceeded(final RepositorySession session) { + doTestStoreRetrieveByGUID(r, session, expectedGUID, record); + } + }); + } catch (InvalidSessionTransitionException e) { + performNotify(e); + } + } + }; + + final Context applicationContext = getApplicationContext(); + + // This has to happen on a new thread so that we + // can wait for it! + Runnable create = onThreadRunnable(new Runnable() { + @Override + public void run() { + r.createSession(createDelegate, applicationContext); + } + }); + + Runnable retrieve = onThreadRunnable(new Runnable() { + @Override + public void run() { + doTestNewSessionRetrieveByTime(r, expectedGUID); + performNotify(); + } + }); + + performWait(create); + performWait(retrieve); + } + + private Runnable onThreadRunnable(final Runnable r) { + return new Runnable() { + @Override + public void run() { + new Thread(r).start(); + } + }; + } + + + public class CountingWBORepository extends TrackingWBORepository { + public AtomicLong counter = new AtomicLong(0L); + public class CountingWBORepositorySession extends WBORepositorySession { + private static final String LOG_TAG = "CountingRepoSession"; + + public CountingWBORepositorySession(WBORepository repository) { + super(repository); + } + + @Override + public void store(final Record record) throws NoStoreDelegateException { + Logger.debug(LOG_TAG, "Counter now " + counter.incrementAndGet()); + super.store(record); + } + } + + @Override + public void createSession(RepositorySessionCreationDelegate delegate, + Context context) { + delegate.deferredCreationDelegate().onSessionCreated(new CountingWBORepositorySession(this)); + } + } + + public class TestRecord extends Record { + public TestRecord(String guid, String collection, long lastModified, + boolean deleted) { + super(guid, collection, lastModified, deleted); + } + + @Override + public void initFromEnvelope(CryptoRecord payload) { + return; + } + + @Override + public CryptoRecord getEnvelope() { + return null; + } + + @Override + protected void populatePayload(ExtendedJSONObject payload) { + } + + @Override + protected void initFromPayload(ExtendedJSONObject payload) { + } + + @Override + public Record copyWithIDs(String guid, long androidID) { + return new TestRecord(guid, this.collection, this.lastModified, this.deleted); + } + } + + /** + * Create two repositories, syncing from one to the other. Ensure + * that records stored from one aren't re-uploaded. + */ + public void testStoreBetweenRepositories() { + final CountingWBORepository repoA = new CountingWBORepository(); // "Remote". First source. + final CountingWBORepository repoB = new CountingWBORepository(); // "Local". First sink. + long now = System.currentTimeMillis(); + + TestRecord recordA1 = new TestRecord("aacdefghiaaa", "coll", now - 30, false); + TestRecord recordA2 = new TestRecord("aacdefghibbb", "coll", now - 20, false); + TestRecord recordB1 = new TestRecord("aacdefghiaaa", "coll", now - 10, false); + TestRecord recordB2 = new TestRecord("aacdefghibbb", "coll", now - 40, false); + + TestRecord recordA3 = new TestRecord("nncdefghibbb", "coll", now, false); + TestRecord recordB3 = new TestRecord("nncdefghiaaa", "coll", now, false); + + // A1 and B1 are the same, but B's version is newer. We expect A1 to be downloaded + // and B1 to be uploaded. + // A2 and B2 are the same, but A's version is newer. We expect A2 to be downloaded + // and B2 to not be uploaded. + // Both A3 and B3 are new. We expect them to go in each direction. + // Expected counts, then: + // Repo A: B1 + B3 + // Repo B: A1 + A2 + A3 + repoB.wbos.put(recordB1.guid, recordB1); + repoB.wbos.put(recordB2.guid, recordB2); + repoB.wbos.put(recordB3.guid, recordB3); + repoA.wbos.put(recordA1.guid, recordA1); + repoA.wbos.put(recordA2.guid, recordA2); + repoA.wbos.put(recordA3.guid, recordA3); + + final Synchronizer s = new Synchronizer(); + s.repositoryA = repoA; + s.repositoryB = repoB; + + Runnable r = new Runnable() { + @Override + public void run() { + s.synchronize(getApplicationContext(), new SynchronizerDelegate() { + + @Override + public void onSynchronized(Synchronizer synchronizer) { + long countA = repoA.counter.get(); + long countB = repoB.counter.get(); + Logger.debug(getName(), "Counts: " + countA + ", " + countB); + assertEq(2L, countA); + assertEq(3L, countB); + + // Testing for store timestamp 'hack'. + // We fetched from A first, and so its bundle timestamp will be the last + // stored time. We fetched from B second, so its bundle timestamp will be + // the last fetched time. + final long timestampA = synchronizer.bundleA.getTimestamp(); + final long timestampB = synchronizer.bundleB.getTimestamp(); + Logger.debug(getName(), "Repo A timestamp: " + timestampA); + Logger.debug(getName(), "Repo B timestamp: " + timestampB); + Logger.debug(getName(), "Repo A fetch done: " + repoA.stats.fetchCompleted); + Logger.debug(getName(), "Repo A store done: " + repoA.stats.storeCompleted); + Logger.debug(getName(), "Repo B fetch done: " + repoB.stats.fetchCompleted); + Logger.debug(getName(), "Repo B store done: " + repoB.stats.storeCompleted); + + assertTrue(timestampB <= timestampA); + assertTrue(repoA.stats.fetchCompleted <= timestampA); + assertTrue(repoA.stats.storeCompleted >= repoA.stats.fetchCompleted); + assertEquals(repoA.stats.storeCompleted, timestampA); + assertEquals(repoB.stats.fetchCompleted, timestampB); + performNotify(); + } + + @Override + public void onSynchronizeFailed(Synchronizer synchronizer, + Exception lastException, String reason) { + Logger.debug(getName(), "Failed."); + performNotify(new AssertionFailedError("Should not fail.")); + } + }); + } + }; + + performWait(onThreadRunnable(r)); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java new file mode 100644 index 000000000..389aaf891 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.sync.SyncConfiguration; + +import android.content.SharedPreferences; + +public class TestSyncConfiguration extends AndroidSyncTestCase { + public static final String TEST_PREFS_NAME = "test"; + + public SharedPreferences getPrefs(String name, int mode) { + return this.getApplicationContext().getSharedPreferences(name, mode); + } + + /** + * Ensure that declined engines persist through prefs. + */ + public void testDeclinedEngineNames() { + SyncConfiguration config = null; + SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0); + + config = newSyncConfiguration(); + config.declinedEngineNames = new HashSet<String>(); + config.declinedEngineNames.add("test1"); + config.declinedEngineNames.add("test2"); + config.persistToPrefs(); + assertTrue(prefs.contains(SyncConfiguration.PREF_DECLINED_ENGINE_NAMES)); + config = newSyncConfiguration(); + Set<String> expected = new HashSet<String>(); + for (String name : new String[] { "test1", "test2" }) { + expected.add(name); + } + assertEquals(expected, config.declinedEngineNames); + + config.declinedEngineNames = null; + config.persistToPrefs(); + assertFalse(prefs.contains(SyncConfiguration.PREF_DECLINED_ENGINE_NAMES)); + config = newSyncConfiguration(); + assertNotNull(config.declinedEngineNames); + assertTrue(config.declinedEngineNames.isEmpty()); + } + + public void testEnabledEngineNames() { + SyncConfiguration config = null; + SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0); + + config = newSyncConfiguration(); + config.enabledEngineNames = new HashSet<String>(); + config.enabledEngineNames.add("test1"); + config.enabledEngineNames.add("test2"); + config.persistToPrefs(); + assertTrue(prefs.contains(SyncConfiguration.PREF_ENABLED_ENGINE_NAMES)); + config = newSyncConfiguration(); + Set<String> expected = new HashSet<String>(); + for (String name : new String[] { "test1", "test2" }) { + expected.add(name); + } + assertEquals(expected, config.enabledEngineNames); + + config.enabledEngineNames = null; + config.persistToPrefs(); + assertFalse(prefs.contains(SyncConfiguration.PREF_ENABLED_ENGINE_NAMES)); + config = newSyncConfiguration(); + assertNull(config.enabledEngineNames); + } + + public void testSyncID() { + SyncConfiguration config = null; + SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0); + + config = newSyncConfiguration(); + config.syncID = "test1"; + config.persistToPrefs(); + assertTrue(prefs.contains(SyncConfiguration.PREF_SYNC_ID)); + config = newSyncConfiguration(); + assertEquals("test1", config.syncID); + } + + public void testStoreSelectedEnginesToPrefs() { + SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0); + // Store engines, excluding history/forms special case. + Map<String, Boolean> expectedEngines = new HashMap<String, Boolean>(); + expectedEngines.put("test1", true); + expectedEngines.put("test2", false); + expectedEngines.put("test3", true); + + SyncConfiguration.storeSelectedEnginesToPrefs(prefs, expectedEngines); + + // Read values from selectedEngines. + assertTrue(prefs.contains(SyncConfiguration.PREF_USER_SELECTED_ENGINES_TO_SYNC)); + SyncConfiguration config = null; + config = newSyncConfiguration(); + config.loadFromPrefs(prefs); + assertEquals(expectedEngines, config.userSelectedEngines); + } + + /** + * Tests dependency of forms engine on history engine. + */ + public void testSelectedEnginesHistoryAndForms() { + SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0); + // Store engines, excluding history/forms special case. + Map<String, Boolean> storedEngines = new HashMap<String, Boolean>(); + storedEngines.put("history", true); + + SyncConfiguration.storeSelectedEnginesToPrefs(prefs, storedEngines); + + // Expected engines. + storedEngines.put("forms", true); + // Read values from selectedEngines. + assertTrue(prefs.contains(SyncConfiguration.PREF_USER_SELECTED_ENGINES_TO_SYNC)); + SyncConfiguration config = null; + config = newSyncConfiguration(); + config.loadFromPrefs(prefs); + assertEquals(storedEngines, config.userSelectedEngines); + } + + public void testsSelectedEnginesNoHistoryNorForms() { + SharedPreferences prefs = getPrefs(TEST_PREFS_NAME, 0); + // Store engines, excluding history/forms special case. + Map<String, Boolean> storedEngines = new HashMap<String, Boolean>(); + storedEngines.put("forms", true); + + SyncConfiguration.storeSelectedEnginesToPrefs(prefs, storedEngines); + + // Read values from selectedEngines. + assertTrue(prefs.contains(SyncConfiguration.PREF_USER_SELECTED_ENGINES_TO_SYNC)); + SyncConfiguration config = null; + config = newSyncConfiguration(); + config.loadFromPrefs(prefs); + // Forms should not be selected if history is not present. + assertTrue(config.userSelectedEngines.isEmpty()); + } + + protected SyncConfiguration newSyncConfiguration() { + return new SyncConfiguration(null, null, getPrefs(TEST_PREFS_NAME, 0)); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestWebURLFinder.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestWebURLFinder.java new file mode 100644 index 000000000..a7ebb71d5 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestWebURLFinder.java @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync; + +import java.util.Arrays; + +import org.mozilla.gecko.background.helpers.AndroidSyncTestCase; +import org.mozilla.gecko.sync.setup.activities.WebURLFinder; + +/** + * These tests are on device because the WebKit APIs are stubs on desktop. + */ +public class TestWebURLFinder extends AndroidSyncTestCase { + public String find(String string) { + return new WebURLFinder(string).bestWebURL(); + } + + public String find(String[] strings) { + return new WebURLFinder(Arrays.asList(strings)).bestWebURL(); + } + + public void testNoEmail() { + assertNull(find("test@test.com")); + } + + public void testSchemeFirst() { + assertEquals("http://scheme.com", find("test.com http://scheme.com")); + } + + public void testFullURL() { + assertEquals("http://scheme.com:8080/inner#anchor&arg=1", find("test.com http://scheme.com:8080/inner#anchor&arg=1")); + } + + public void testNoScheme() { + assertEquals("noscheme.com", find("noscheme.com")); + } + + public void testNoBadScheme() { + assertNull(find("file:///test javascript:///test.js")); + } + + public void testStrings() { + assertEquals("http://test.com", find(new String[] { "http://test.com", "noscheme.com" })); + assertEquals("http://test.com", find(new String[] { "noschemefirst.com", "http://test.com" })); + assertEquals("http://test.com/inner#test", find(new String[] { "noschemefirst.com", "http://test.com/inner#test", "http://second.org/fark" })); + assertEquals("http://test.com", find(new String[] { "javascript:///test.js", "http://test.com" })); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java new file mode 100644 index 000000000..0fcd762f4 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.json.simple.JSONArray; +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; + +public class BookmarkHelpers { + + private static String mobileFolderGuid = "mobile"; + private static String mobileFolderName = "mobile"; + private static String topFolderGuid = Utils.generateGuid(); + private static String topFolderName = "My Top Folder"; + private static String middleFolderGuid = Utils.generateGuid(); + private static String middleFolderName = "My Middle Folder"; + private static String bottomFolderGuid = Utils.generateGuid(); + private static String bottomFolderName = "My Bottom Folder"; + private static String bmk1Guid = Utils.generateGuid(); + private static String bmk2Guid = Utils.generateGuid(); + private static String bmk3Guid = Utils.generateGuid(); + private static String bmk4Guid = Utils.generateGuid(); + + /* + * Helpers for creating bookmark records of different types + */ + public static BookmarkRecord createBookmarkInMobileFolder1() { + BookmarkRecord rec = createBookmark1(); + rec.guid = Utils.generateGuid(); + rec.parentID = mobileFolderGuid; + rec.parentName = mobileFolderName; + return rec; + } + + public static BookmarkRecord createBookmarkInMobileFolder2() { + BookmarkRecord rec = createBookmark2(); + rec.guid = Utils.generateGuid(); + rec.parentID = mobileFolderGuid; + rec.parentName = mobileFolderName; + return rec; + } + + @SuppressWarnings("unchecked") + public static BookmarkRecord createBookmark1() { + BookmarkRecord record = new BookmarkRecord(); + JSONArray tags = new JSONArray(); + tags.add("tag1"); + tags.add("tag2"); + tags.add("tag3"); + record.guid = bmk1Guid; + record.title = "Foo!!!"; + record.bookmarkURI = "http://foo.bar.com"; + record.description = "This is a description for foo.bar.com"; + record.tags = tags; + record.keyword = "fooooozzzzz"; + record.parentID = topFolderGuid; + record.parentName = topFolderName; + record.type = "bookmark"; + return record; + } + + @SuppressWarnings("unchecked") + public static BookmarkRecord createBookmark2() { + BookmarkRecord record = new BookmarkRecord(); + JSONArray tags = new JSONArray(); + tags.add("tag1"); + tags.add("tag2"); + record.guid = bmk2Guid; + record.title = "Bar???"; + record.bookmarkURI = "http://bar.foo.com"; + record.description = "This is a description for Bar???"; + record.tags = tags; + record.keyword = "keywordzzz"; + record.parentID = topFolderGuid; + record.parentName = topFolderName; + record.type = "bookmark"; + return record; + } + + @SuppressWarnings("unchecked") + public static BookmarkRecord createBookmark3() { + BookmarkRecord record = new BookmarkRecord(); + JSONArray tags = new JSONArray(); + tags.add("tag1"); + tags.add("tag2"); + record.guid = bmk3Guid; + record.title = "Bmk3"; + record.bookmarkURI = "http://bmk3.com"; + record.description = "This is a description for bmk3"; + record.tags = tags; + record.keyword = "snooozzz"; + record.parentID = middleFolderGuid; + record.parentName = middleFolderName; + record.type = "bookmark"; + return record; + } + + @SuppressWarnings("unchecked") + public static BookmarkRecord createBookmark4() { + BookmarkRecord record = new BookmarkRecord(); + JSONArray tags = new JSONArray(); + tags.add("tag1"); + tags.add("tag2"); + record.guid = bmk4Guid; + record.title = "Bmk4"; + record.bookmarkURI = "http://bmk4.com"; + record.description = "This is a description for bmk4?"; + record.tags = tags; + record.keyword = "booooozzz"; + record.parentID = bottomFolderGuid; + record.parentName = bottomFolderName; + record.type = "bookmark"; + return record; + } + + @SuppressWarnings("unchecked") + public static BookmarkRecord createMicrosummary() { + BookmarkRecord record = new BookmarkRecord(); + JSONArray tags = new JSONArray(); + tags.add("tag1"); + tags.add("tag2"); + record.guid = Utils.generateGuid(); + record.title = "Microsummary 1"; + record.bookmarkURI = "www.bmkuri.com"; + record.description = "microsummary description"; + record.tags = tags; + record.keyword = "keywordzzz"; + record.parentID = topFolderGuid; + record.parentName = topFolderName; + record.type = "microsummary"; + return record; + } + + public static BookmarkRecord createQuery() { + BookmarkRecord record = new BookmarkRecord(); + record.guid = Utils.generateGuid(); + record.title = "Query 1"; + record.bookmarkURI = "http://www.query.com"; + record.description = "Query 1 description"; + record.tags = new JSONArray(); + record.keyword = "queryKeyword"; + record.parentID = topFolderGuid; + record.parentName = topFolderName; + record.type = "query"; + return record; + } + + @SuppressWarnings("unchecked") + public static BookmarkRecord createFolder1() { + BookmarkRecord record = new BookmarkRecord(); + record.guid = topFolderGuid; + record.title = topFolderName; + record.parentID = "mobile"; + record.parentName = "mobile"; + JSONArray children = new JSONArray(); + children.add(bmk1Guid); + children.add(bmk2Guid); + record.children = children; + record.type = "folder"; + return record; + } + + @SuppressWarnings("unchecked") + public static BookmarkRecord createFolder2() { + BookmarkRecord record = new BookmarkRecord(); + record.guid = middleFolderGuid; + record.title = middleFolderName; + record.parentID = topFolderGuid; + record.parentName = topFolderName; + JSONArray children = new JSONArray(); + children.add(bmk3Guid); + record.children = children; + record.type = "folder"; + return record; + } + + @SuppressWarnings("unchecked") + public static BookmarkRecord createFolder3() { + BookmarkRecord record = new BookmarkRecord(); + record.guid = bottomFolderGuid; + record.title = bottomFolderName; + record.parentID = middleFolderGuid; + record.parentName = middleFolderName; + JSONArray children = new JSONArray(); + children.add(bmk4Guid); + record.children = children; + record.type = "folder"; + return record; + } + + @SuppressWarnings("unchecked") + public static BookmarkRecord createLivemark() { + BookmarkRecord record = new BookmarkRecord(); + record.guid = Utils.generateGuid(); + record.title = "Livemark title"; + record.parentID = topFolderGuid; + record.parentName = topFolderName; + JSONArray children = new JSONArray(); + children.add(Utils.generateGuid()); + children.add(Utils.generateGuid()); + record.children = children; + record.type = "livemark"; + return record; + } + + public static BookmarkRecord createSeparator() { + BookmarkRecord record = new BookmarkRecord(); + record.guid = Utils.generateGuid(); + record.androidPosition = 3; + record.parentID = topFolderGuid; + record.parentName = topFolderName; + record.type = "separator"; + return record; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java new file mode 100644 index 000000000..67aa81fff --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import java.util.concurrent.ExecutorService; + +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; + +public class DefaultBeginDelegate extends DefaultDelegate implements RepositorySessionBeginDelegate { + @Override + public void onBeginFailed(Exception ex) { + performNotify("Begin failed", ex); + } + + @Override + public void onBeginSucceeded(RepositorySession session) { + performNotify("Default begin delegate hit.", null); + } + + @Override + public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) { + DefaultBeginDelegate copy; + try { + copy = (DefaultBeginDelegate) this.clone(); + copy.executor = executor; + return copy; + } catch (CloneNotSupportedException e) { + return this; + } + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java new file mode 100644 index 000000000..a1f5b7a97 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.mozilla.gecko.sync.repositories.Repository; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCleanDelegate; + +public class DefaultCleanDelegate extends DefaultDelegate implements RepositorySessionCleanDelegate { + + @Override + public void onCleaned(Repository repo) { + performNotify("Default begin delegate hit.", null); + } + + @Override + public void onCleanFailed(Repository repo, Exception ex) { + performNotify("Clean failed.", ex); + } + +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java new file mode 100644 index 000000000..7e9341f02 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import java.util.concurrent.ExecutorService; + +import junit.framework.AssertionFailedError; + +import org.mozilla.gecko.background.testhelpers.WaitHelper; + +public abstract class DefaultDelegate { + protected ExecutorService executor; + + protected final WaitHelper waitHelper; + + public DefaultDelegate() { + waitHelper = WaitHelper.getTestWaiter(); + } + + public DefaultDelegate(WaitHelper waitHelper) { + this.waitHelper = waitHelper; + } + + protected WaitHelper getTestWaiter() { + return waitHelper; + } + + public void performWait(Runnable runnable) throws AssertionFailedError { + getTestWaiter().performWait(runnable); + } + + public void performNotify() { + getTestWaiter().performNotify(); + } + + public void performNotify(Throwable e) { + getTestWaiter().performNotify(e); + } + + public void performNotify(String reason, Throwable e) { + String message = reason; + if (e != null) { + message += ": " + e.getMessage(); + } + AssertionFailedError ex = new AssertionFailedError(message); + if (e != null) { + ex.initCause(e); + } + getTestWaiter().performNotify(ex); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java new file mode 100644 index 000000000..3d7d23bab --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutorService; + +import junit.framework.AssertionFailedError; + +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.sync.repositories.delegates.DeferredRepositorySessionFetchRecordsDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; +import org.mozilla.gecko.sync.repositories.domain.Record; + +public class DefaultFetchDelegate extends DefaultDelegate implements RepositorySessionFetchRecordsDelegate { + + private static final String LOG_TAG = "DefaultFetchDelegate"; + public ArrayList<Record> records = new ArrayList<Record>(); + public Set<String> ignore = new HashSet<String>(); + + @Override + public void onFetchFailed(Exception ex, Record record) { + performNotify("Fetch failed.", ex); + } + + protected void onDone(ArrayList<Record> records, HashMap<String, Record> expected, long end) { + Logger.debug(LOG_TAG, "onDone."); + Logger.debug(LOG_TAG, "End timestamp is " + end); + Logger.debug(LOG_TAG, "Expected is " + expected); + Logger.debug(LOG_TAG, "Records is " + records); + Set<String> foundGuids = new HashSet<String>(); + try { + int expectedCount = 0; + int expectedFound = 0; + Logger.debug(LOG_TAG, "Counting expected keys."); + for (String key : expected.keySet()) { + if (!ignore.contains(key)) { + expectedCount++; + } + } + Logger.debug(LOG_TAG, "Expected keys: " + expectedCount); + for (Record record : records) { + Logger.debug(LOG_TAG, "Record."); + Logger.debug(LOG_TAG, record.guid); + + // Ignore special GUIDs (e.g., for bookmarks). + if (!ignore.contains(record.guid)) { + if (foundGuids.contains(record.guid)) { + fail("Found duplicate guid " + record.guid); + } + Record expect = expected.get(record.guid); + if (expect == null) { + fail("Do not expect to get back a record with guid: " + record.guid); // Caught below + } + Logger.debug(LOG_TAG, "Checking equality."); + try { + assertTrue(expect.equalPayloads(record)); // Caught below + } catch (Exception e) { + Logger.error(LOG_TAG, "ONOZ!", e); + } + Logger.debug(LOG_TAG, "Checked equality."); + expectedFound += 1; + // Track record once we've found it. + foundGuids.add(record.guid); + } + } + assertEquals(expectedCount, expectedFound); // Caught below + Logger.debug(LOG_TAG, "Notifying success."); + performNotify(); + } catch (AssertionFailedError e) { + Logger.error(LOG_TAG, "Notifying assertion failure."); + performNotify(e); + } catch (Exception e) { + Logger.error(LOG_TAG, "No!"); + performNotify(); + } + } + + public int recordCount() { + return (this.records == null) ? 0 : this.records.size(); + } + + @Override + public void onFetchedRecord(Record record) { + Logger.debug(LOG_TAG, "onFetchedRecord(" + record.guid + ")"); + records.add(record); + } + + @Override + public void onFetchCompleted(final long fetchEnd) { + Logger.debug(LOG_TAG, "onFetchCompleted. Doing nothing."); + } + + @Override + public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(final ExecutorService executor) { + return new DeferredRepositorySessionFetchRecordsDelegate(this, executor); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java new file mode 100644 index 000000000..11e451e82 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import java.util.concurrent.ExecutorService; + +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; + +public class DefaultFinishDelegate extends DefaultDelegate implements RepositorySessionFinishDelegate { + + @Override + public void onFinishFailed(Exception ex) { + performNotify("Finish failed", ex); + } + + @Override + public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) { + performNotify("Hit default finish delegate", null); + } + + @Override + public RepositorySessionFinishDelegate deferredFinishDelegate(final ExecutorService executor) { + final RepositorySessionFinishDelegate self = this; + + Logger.info("DefaultFinishDelegate", "Deferring…"); + return new RepositorySessionFinishDelegate() { + @Override + public void onFinishSucceeded(final RepositorySession session, + final RepositorySessionBundle bundle) { + Logger.info("DefaultFinishDelegate", "Executing onFinishSucceeded Runnable…"); + executor.execute(new Runnable() { + @Override + public void run() { + self.onFinishSucceeded(session, bundle); + }}); + } + + @Override + public void onFinishFailed(final Exception ex) { + executor.execute(new Runnable() { + @Override + public void run() { + self.onFinishFailed(ex); + }}); + } + + @Override + public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService newExecutor) { + if (newExecutor == executor) { + return this; + } + throw new IllegalArgumentException("Can't re-defer this delegate."); + } + }; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java new file mode 100644 index 000000000..78e3cc84f --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; + +public class DefaultGuidsSinceDelegate extends DefaultDelegate implements RepositorySessionGuidsSinceDelegate { + + @Override + public void onGuidsSinceFailed(Exception ex) { + performNotify("shouldn't fail", ex); + } + + @Override + public void onGuidsSinceSucceeded(String[] guids) { + performNotify("default guids since delegate called", null); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java new file mode 100644 index 000000000..5d52df84d --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; + +public class DefaultSessionCreationDelegate extends DefaultDelegate implements + RepositorySessionCreationDelegate { + + @Override + public void onSessionCreateFailed(Exception ex) { + performNotify("Session creation failed", ex); + } + + @Override + public void onSessionCreated(RepositorySession session) { + performNotify("Should not have been created.", null); + } + + @Override + public RepositorySessionCreationDelegate deferredCreationDelegate() { + final RepositorySessionCreationDelegate self = this; + return new RepositorySessionCreationDelegate() { + + @Override + public void onSessionCreated(final RepositorySession session) { + new Thread(new Runnable() { + @Override + public void run() { + self.onSessionCreated(session); + } + }).start(); + } + + @Override + public void onSessionCreateFailed(final Exception ex) { + new Thread(new Runnable() { + @Override + public void run() { + self.onSessionCreateFailed(ex); + } + }).start(); + } + + @Override + public RepositorySessionCreationDelegate deferredCreationDelegate() { + return this; + } + }; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java new file mode 100644 index 000000000..7ba2e6df6 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import java.util.concurrent.ExecutorService; + +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; + +public class DefaultStoreDelegate extends DefaultDelegate implements RepositorySessionStoreDelegate { + + @Override + public void onRecordStoreFailed(Exception ex, String guid) { + performNotify("Store failed", ex); + } + + @Override + public void onRecordStoreSucceeded(String guid) { + performNotify("DefaultStoreDelegate used", null); + } + + @Override + public void onStoreCompleted(long storeEnd) { + performNotify("DefaultStoreDelegate used", null); + } + + @Override + public RepositorySessionStoreDelegate deferredStoreDelegate(final ExecutorService executor) { + final RepositorySessionStoreDelegate self = this; + return new RepositorySessionStoreDelegate() { + + @Override + public void onRecordStoreSucceeded(final String guid) { + executor.execute(new Runnable() { + @Override + public void run() { + self.onRecordStoreSucceeded(guid); + } + }); + } + + @Override + public void onRecordStoreFailed(final Exception ex, final String guid) { + executor.execute(new Runnable() { + @Override + public void run() { + self.onRecordStoreFailed(ex, guid); + } + }); + } + + @Override + public void onStoreCompleted(final long storeEnd) { + executor.execute(new Runnable() { + @Override + public void run() { + self.onStoreCompleted(storeEnd); + } + }); + } + + @Override + public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService newExecutor) { + if (newExecutor == executor) { + return this; + } + throw new IllegalArgumentException("Can't re-defer this delegate."); + } + }; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java new file mode 100644 index 000000000..d320cfd7a --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import static junit.framework.Assert.assertNotNull; +import junit.framework.AssertionFailedError; + +import org.mozilla.gecko.sync.repositories.RepositorySession; + +public class ExpectBeginDelegate extends DefaultBeginDelegate { + @Override + public void onBeginSucceeded(RepositorySession session) { + try { + assertNotNull(session); + } catch (AssertionFailedError e) { + performNotify("Expected non-null session", e); + return; + } + performNotify(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java new file mode 100644 index 000000000..ff1807d51 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; + +public class ExpectBeginFailDelegate extends DefaultBeginDelegate { + + @Override + public void onBeginFailed(Exception ex) { + if (!(ex instanceof InvalidSessionTransitionException)) { + performNotify("Expected InvalidSessionTransititionException but got ", ex); + } + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java new file mode 100644 index 000000000..5cfe5327a --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import java.util.HashMap; + +import org.mozilla.gecko.sync.repositories.domain.Record; + +public class ExpectFetchDelegate extends DefaultFetchDelegate { + private HashMap<String, Record> expect = new HashMap<String, Record>(); + + public ExpectFetchDelegate(Record[] records) { + for(int i = 0; i < records.length; i++) { + expect.put(records[i].guid, records[i]); + } + } + + @Override + public void onFetchedRecord(Record record) { + this.records.add(record); + } + + @Override + public void onFetchCompleted(final long fetchEnd) { + super.onDone(this.records, this.expect, fetchEnd); + } + + public Record recordAt(int i) { + return this.records.get(i); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java new file mode 100644 index 000000000..7dcada0d4 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import java.util.Arrays; + +import org.mozilla.gecko.sync.repositories.domain.Record; + +import junit.framework.AssertionFailedError; + +public class ExpectFetchSinceDelegate extends DefaultFetchDelegate { + private String[] expected; + private long earliest; + + public ExpectFetchSinceDelegate(long timestamp, String[] guids) { + expected = guids; + earliest = timestamp; + Arrays.sort(expected); + } + + @Override + public void onFetchCompleted(final long fetchEnd) { + AssertionFailedError err = null; + try { + int countSpecials = 0; + for (Record record : records) { + // Check if record should be ignored. + if (!ignore.contains(record.guid)) { + assertFalse(-1 == Arrays.binarySearch(this.expected, record.guid)); + } else { + countSpecials++; + } + // Check that record is later than timestamp-earliest. + assertTrue(record.lastModified >= this.earliest); + } + assertEquals(this.expected.length, records.size() - countSpecials); + } catch (AssertionFailedError e) { + err = e; + } + performNotify(err); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java new file mode 100644 index 000000000..0b6f1de88 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.sync.repositories.RepositorySession; +import org.mozilla.gecko.sync.repositories.RepositorySessionBundle; + +public class ExpectFinishDelegate extends DefaultFinishDelegate { + + @Override + public void onFinishSucceeded(RepositorySession session, RepositorySessionBundle bundle) { + Logger.info("ExpectFinishDelegate", "Finish succeeded."); + performNotify(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java new file mode 100644 index 000000000..df83432bc --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; + +public class ExpectFinishFailDelegate extends DefaultFinishDelegate { + @Override + public void onFinishFailed(Exception ex) { + if (!(ex instanceof InvalidSessionTransitionException)) { + performNotify("Expected InvalidSessionTransititionException but got ", ex); + } + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java new file mode 100644 index 000000000..435ba7502 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import junit.framework.AssertionFailedError; + +public class ExpectGuidsSinceDelegate extends DefaultGuidsSinceDelegate { + private String[] expected; + public Set<String> ignore = new HashSet<String>(); + + public ExpectGuidsSinceDelegate(String[] guids) { + expected = guids; + Arrays.sort(expected); + } + + @Override + public void onGuidsSinceSucceeded(String[] guids) { + AssertionFailedError err = null; + try { + int notIgnored = 0; + for (String guid : guids) { + if (!ignore.contains(guid)) { + notIgnored++; + assertFalse(-1 == Arrays.binarySearch(this.expected, guid)); + } + } + assertEquals(this.expected.length, notIgnored); + } catch (AssertionFailedError e) { + err = e; + } + performNotify(err); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java new file mode 100644 index 000000000..73035869c --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.mozilla.gecko.sync.repositories.InvalidRequestException; +import org.mozilla.gecko.sync.repositories.domain.Record; + +public class ExpectInvalidRequestFetchDelegate extends DefaultFetchDelegate { + public static final String LOG_TAG = "ExpInvRequestFetchDel"; + + @Override + public void onFetchFailed(Exception ex, Record rec) { + if (ex instanceof InvalidRequestException) { + onDone(); + } else { + performNotify("Expected InvalidRequestException but got ", ex); + } + } + + private void onDone() { + performNotify(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java new file mode 100644 index 000000000..5ce56ee5f --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import static junit.framework.Assert.assertEquals; + +import org.mozilla.gecko.sync.repositories.InvalidBookmarkTypeException; + +public class ExpectInvalidTypeStoreDelegate extends DefaultStoreDelegate { + + @Override + public void onRecordStoreFailed(Exception ex, String guid) { + assertEquals(InvalidBookmarkTypeException.class, ex.getClass()); + performNotify(); + } + +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java new file mode 100644 index 000000000..2a8df0228 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; + +import java.util.HashSet; +import java.util.concurrent.atomic.AtomicLong; + +import junit.framework.AssertionFailedError; + +import org.mozilla.gecko.sync.repositories.domain.Record; + +public class ExpectManyStoredDelegate extends DefaultStoreDelegate { + HashSet<String> expectedGUIDs; + AtomicLong stored; + + public ExpectManyStoredDelegate(Record[] records) { + HashSet<String> s = new HashSet<String>(); + for (Record record : records) { + s.add(record.guid); + } + expectedGUIDs = s; + stored = new AtomicLong(0); + } + + @Override + public void onStoreCompleted(long storeEnd) { + try { + assertEquals(expectedGUIDs.size(), stored.get()); + performNotify(); + } catch (AssertionFailedError e) { + performNotify(e); + } + } + + @Override + public void onRecordStoreSucceeded(String guid) { + try { + assertTrue(expectedGUIDs.contains(guid)); + } catch (AssertionFailedError e) { + performNotify(e); + } + stored.incrementAndGet(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java new file mode 100644 index 000000000..a9f11d7b0 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import static junit.framework.Assert.assertEquals; + +import java.util.HashSet; +import java.util.Set; + +import junit.framework.AssertionFailedError; + +public class ExpectNoGUIDsSinceDelegate extends DefaultGuidsSinceDelegate { + + public Set<String> ignore = new HashSet<String>(); + + @Override + public void onGuidsSinceSucceeded(String[] guids) { + AssertionFailedError err = null; + try { + int nonIgnored = 0; + for (int i = 0; i < guids.length; i++) { + if (!ignore.contains(guids[i])) { + nonIgnored++; + } + } + assertEquals(0, nonIgnored); + } catch (AssertionFailedError e) { + err = e; + } + performNotify(err); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java new file mode 100644 index 000000000..93626898e --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +public class ExpectNoStoreDelegate extends ExpectStoreCompletedDelegate { + @Override + public void onRecordStoreSucceeded(String guid) { + performNotify("Should not have stored record " + guid, null); + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java new file mode 100644 index 000000000..b3cc909a1 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +public class ExpectStoreCompletedDelegate extends DefaultStoreDelegate { + + @Override + public void onRecordStoreSucceeded(String guid) { + // That's fine. + } + + @Override + public void onStoreCompleted(long storeEnd) { + performNotify(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java new file mode 100644 index 000000000..dc2e8a2d1 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import junit.framework.AssertionFailedError; + +public class ExpectStoredDelegate extends DefaultStoreDelegate { + String expectedGUID; + String storedGuid; + + public ExpectStoredDelegate(String guid) { + this.expectedGUID = guid; + } + + @Override + public synchronized void onStoreCompleted(long storeEnd) { + try { + assertNotNull(storedGuid); + performNotify(); + } catch (AssertionFailedError e) { + performNotify("GUID " + this.expectedGUID + " was not stored", e); + } + } + + @Override + public synchronized void onRecordStoreSucceeded(String guid) { + this.storedGuid = guid; + try { + if (this.expectedGUID != null) { + assertEquals(this.expectedGUID, guid); + } + } catch (AssertionFailedError e) { + performNotify(e); + } + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java new file mode 100644 index 000000000..68f80043e --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.mozilla.gecko.sync.repositories.domain.HistoryRecord; + +public class HistoryHelpers { + + @SuppressWarnings("unchecked") + private static JSONArray getVisits1() { + JSONArray json = new JSONArray(); + JSONObject obj = new JSONObject(); + obj.put("date", 1320087601465600000L); + obj.put("type", 2L); + json.add(obj); + obj = new JSONObject(); + obj.put("date", 1320084970724990000L); + obj.put("type", 1L); + json.add(obj); + obj = new JSONObject(); + obj.put("date", 1319764134412287000L); + obj.put("type", 1L); + json.add(obj); + obj = new JSONObject(); + obj.put("date", 1319681306455594000L); + obj.put("type", 2L); + json.add(obj); + return json; + } + + @SuppressWarnings("unchecked") + private static JSONArray getVisits2() { + JSONArray json = new JSONArray(); + JSONObject obj = new JSONObject(); + obj = new JSONObject(); + obj.put("date", 1319764134412345000L); + obj.put("type", 4L); + json.add(obj); + obj = new JSONObject(); + obj.put("date", 1319681306454321000L); + obj.put("type", 3L); + json.add(obj); + return json; + } + + public static HistoryRecord createHistory1() { + HistoryRecord record = new HistoryRecord(); + record.title = "History 1"; + record.histURI = "http://history.page1.com"; + record.visits = getVisits1(); + return record; + } + + + public static HistoryRecord createHistory2() { + HistoryRecord record = new HistoryRecord(); + record.title = "History 2"; + record.histURI = "http://history.page2.com"; + record.visits = getVisits2(); + return record; + } + + public static HistoryRecord createHistory3() { + HistoryRecord record = new HistoryRecord(); + record.title = "History 3"; + record.histURI = "http://history.page3.com"; + record.visits = getVisits2(); + return record; + } + + public static HistoryRecord createHistory4() { + HistoryRecord record = new HistoryRecord(); + record.title = "History 4"; + record.histURI = "http://history.page4.com"; + record.visits = getVisits1(); + return record; + } + + public static HistoryRecord createHistory5() { + HistoryRecord record = new HistoryRecord(); + record.title = "History 5"; + record.histURI = "http://history.page5.com"; + record.visits = getVisits2(); + return record; + } + +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java new file mode 100644 index 000000000..87400a2b0 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.mozilla.gecko.sync.repositories.domain.PasswordRecord; + +public class PasswordHelpers { + + public static PasswordRecord createPassword1() { + PasswordRecord rec = new PasswordRecord(); + rec.encType = "some type"; + rec.formSubmitURL = "http://submit.html"; + rec.hostname = "http://hostname"; + rec.httpRealm = "httpRealm"; + rec.encryptedPassword ="12345"; + rec.passwordField = "box.pass.field"; + rec.timeCreated = 111111111L; + rec.timeLastUsed = 123412352435L; + rec.timePasswordChanged = 121111111L; + rec.timesUsed = 5L; + rec.encryptedUsername = "jvoll"; + rec.usernameField = "box.user.field"; + return rec; + } + + public static PasswordRecord createPassword2() { + PasswordRecord rec = new PasswordRecord(); + rec.encType = "some type"; + rec.formSubmitURL = "http://submit2.html"; + rec.hostname = "http://hostname2"; + rec.httpRealm = "httpRealm2"; + rec.encryptedPassword ="54321"; + rec.passwordField = "box.pass.field2"; + rec.timeCreated = 12111111111L; + rec.timeLastUsed = 123412352213L; + rec.timePasswordChanged = 123111111111L; + rec.timesUsed = 2L; + rec.encryptedUsername = "rnewman"; + rec.usernameField = "box.user.field2"; + return rec; + } + + public static PasswordRecord createPassword3() { + PasswordRecord rec = new PasswordRecord(); + rec.encType = "some type3"; + rec.formSubmitURL = "http://submit3.html"; + rec.hostname = "http://hostname3"; + rec.httpRealm = "httpRealm3"; + rec.encryptedPassword ="54321"; + rec.passwordField = "box.pass.field3"; + rec.timeCreated = 100000000000L; + rec.timeLastUsed = 123412352213L; + rec.timePasswordChanged = 110000000000L; + rec.timesUsed = 2L; + rec.encryptedUsername = "rnewman"; + rec.usernameField = "box.user.field3"; + return rec; + } + + public static PasswordRecord createPassword4() { + PasswordRecord rec = new PasswordRecord(); + rec.encType = "some type"; + rec.formSubmitURL = "http://submit4.html"; + rec.hostname = "http://hostname4"; + rec.httpRealm = "httpRealm4"; + rec.encryptedPassword ="54324"; + rec.passwordField = "box.pass.field4"; + rec.timeCreated = 101000000000L; + rec.timeLastUsed = 123412354444L; + rec.timePasswordChanged = 110000000000L; + rec.timesUsed = 4L; + rec.encryptedUsername = "rnewman4"; + rec.usernameField = "box.user.field4"; + return rec; + } + + public static PasswordRecord createPassword5() { + PasswordRecord rec = new PasswordRecord(); + rec.encType = "some type5"; + rec.formSubmitURL = "http://submit5.html"; + rec.hostname = "http://hostname5"; + rec.httpRealm = "httpRealm5"; + rec.encryptedPassword ="54325"; + rec.passwordField = "box.pass.field5"; + rec.timeCreated = 101000000000L; + rec.timeLastUsed = 123412352555L; + rec.timePasswordChanged = 111111111111L; + rec.timesUsed = 5L; + rec.encryptedUsername = "jvoll5"; + rec.usernameField = "box.user.field5"; + return rec; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java new file mode 100644 index 000000000..9c9e6719b --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import static junit.framework.Assert.assertNotNull; + +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.testhelpers.WaitHelper; +import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; +import org.mozilla.gecko.sync.repositories.Repository; +import org.mozilla.gecko.sync.repositories.RepositorySession; + +import android.content.Context; + +public class SessionTestHelper { + + protected static RepositorySession prepareRepositorySession( + final Context context, + final boolean begin, + final Repository repository) { + + final WaitHelper testWaiter = WaitHelper.getTestWaiter(); + + final String logTag = "prepareRepositorySession"; + class CreationDelegate extends DefaultSessionCreationDelegate { + private RepositorySession session; + synchronized void setSession(RepositorySession session) { + this.session = session; + } + synchronized RepositorySession getSession() { + return this.session; + } + + @Override + public void onSessionCreated(final RepositorySession session) { + assertNotNull(session); + Logger.info(logTag, "Setting session to " + session); + setSession(session); + if (begin) { + Logger.info(logTag, "Calling session.begin on new session."); + // The begin callbacks will notify. + try { + session.begin(new ExpectBeginDelegate()); + } catch (InvalidSessionTransitionException e) { + testWaiter.performNotify(e); + } + } else { + Logger.info(logTag, "Notifying after setting new session."); + testWaiter.performNotify(); + } + } + } + + final CreationDelegate delegate = new CreationDelegate(); + try { + Runnable runnable = new Runnable() { + @Override + public void run() { + repository.createSession(delegate, context); + } + }; + testWaiter.performWait(runnable); + } catch (IllegalArgumentException ex) { + Logger.warn(logTag, "Caught IllegalArgumentException."); + } + + Logger.info(logTag, "Retrieving new session."); + final RepositorySession session = delegate.getSession(); + assertNotNull(session); + + return session; + } + + public static RepositorySession createSession(final Context context, final Repository repository) { + return prepareRepositorySession(context, false, repository); + } + + public static RepositorySession createAndBeginSession(Context context, Repository repository) { + return prepareRepositorySession(context, true, repository); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java new file mode 100644 index 000000000..0eb477be7 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import java.util.concurrent.ExecutorService; + +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; + +public abstract class SimpleSuccessBeginDelegate extends DefaultDelegate implements RepositorySessionBeginDelegate { + @Override + public void onBeginFailed(Exception ex) { + performNotify("Begin failed", ex); + } + + @Override + public RepositorySessionBeginDelegate deferredBeginDelegate(ExecutorService executor) { + return this; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java new file mode 100644 index 000000000..3b3b3d5fa --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; + +public abstract class SimpleSuccessCreationDelegate extends DefaultDelegate implements RepositorySessionCreationDelegate { + @Override + public void onSessionCreateFailed(Exception ex) { + performNotify("Session creation failed", ex); + } + + @Override + public RepositorySessionCreationDelegate deferredCreationDelegate() { + return this; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java new file mode 100644 index 000000000..f0e9428ba --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import java.util.concurrent.ExecutorService; + +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; +import org.mozilla.gecko.sync.repositories.domain.Record; + +public abstract class SimpleSuccessFetchDelegate extends DefaultDelegate implements + RepositorySessionFetchRecordsDelegate { + @Override + public void onFetchFailed(Exception ex, Record record) { + performNotify("Fetch failed", ex); + } + + @Override + public RepositorySessionFetchRecordsDelegate deferredFetchDelegate(ExecutorService executor) { + return this; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java new file mode 100644 index 000000000..5ac1bcde7 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import java.util.concurrent.ExecutorService; + +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; + +public abstract class SimpleSuccessFinishDelegate extends DefaultDelegate implements RepositorySessionFinishDelegate { + @Override + public void onFinishFailed(Exception ex) { + performNotify("Finish failed", ex); + } + + @Override + public RepositorySessionFinishDelegate deferredFinishDelegate(ExecutorService executor) { + return this; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java new file mode 100644 index 000000000..c725c4a46 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java @@ -0,0 +1,20 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.sync.helpers; + +import java.util.concurrent.ExecutorService; + +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionStoreDelegate; + +public abstract class SimpleSuccessStoreDelegate extends DefaultDelegate implements RepositorySessionStoreDelegate { + @Override + public void onRecordStoreFailed(Exception ex, String guid) { + performNotify("Store failed", ex); + } + + @Override + public RepositorySessionStoreDelegate deferredStoreDelegate(ExecutorService executor) { + return this; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java new file mode 100644 index 000000000..d2a8b8476 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.NoCollectionKeysSetException; +import org.mozilla.gecko.sync.SynchronizerConfiguration; +import org.mozilla.gecko.sync.repositories.RecordFactory; +import org.mozilla.gecko.sync.repositories.Repository; +import org.mozilla.gecko.sync.stage.ServerSyncStage; + +import java.net.URISyntaxException; + +/** + * A stage that joins two Repositories with no wrapping. + */ +public abstract class BaseMockServerSyncStage extends ServerSyncStage { + + public Repository local; + public Repository remote; + public String name; + public String collection; + public int version = 1; + + @Override + public boolean isEnabled() { + return true; + } + + @Override + protected String getCollection() { + return collection; + } + + @Override + protected Repository getLocalRepository() { + return local; + } + + @Override + protected Repository getRemoteRepository() throws URISyntaxException { + return remote; + } + + @Override + protected String getEngineName() { + return name; + } + + @Override + public Integer getStorageVersion() { + return version; + } + + @Override + protected RecordFactory getRecordFactory() { + return null; + } + + @Override + protected Repository wrappedServerRepo() + throws NoCollectionKeysSetException, URISyntaxException { + return getRemoteRepository(); + } + + public SynchronizerConfiguration leakConfig() throws Exception { + return this.getConfig(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java new file mode 100644 index 000000000..48217f1b0 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.json.simple.JSONArray; +import org.mozilla.gecko.sync.CommandProcessor.Command; + +public class CommandHelpers { + + @SuppressWarnings("unchecked") + public static Command getCommand1() { + JSONArray args = new JSONArray(); + args.add("argsA"); + return new Command("displayURI", args); + } + + @SuppressWarnings("unchecked") + public static Command getCommand2() { + JSONArray args = new JSONArray(); + args.add("argsB"); + return new Command("displayURI", args); + } + + @SuppressWarnings("unchecked") + public static Command getCommand3() { + JSONArray args = new JSONArray(); + args.add("argsC"); + return new Command("displayURI", args); + } + + @SuppressWarnings("unchecked") + public static Command getCommand4() { + JSONArray args = new JSONArray(); + args.add("URI of Page"); + args.add("Sender ID"); + args.add("Title of Page"); + return new Command("displayURI", args); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java new file mode 100644 index 000000000..c8be7e330 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import java.net.URI; + +import org.mozilla.gecko.sync.GlobalSession; +import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; +import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; + +public class DefaultGlobalSessionCallback implements GlobalSessionCallback { + + @Override + public void requestBackoff(long backoff) { + } + + @Override + public void informUnauthorizedResponse(GlobalSession globalSession, + URI oldClusterURL) { + } + + @Override + public void informUpgradeRequiredResponse(GlobalSession session) { + } + + @Override + public void informMigrated(GlobalSession globalSession) { + } + + @Override + public void handleAborted(GlobalSession globalSession, String reason) { + } + + @Override + public void handleError(GlobalSession globalSession, Exception ex) { + } + + @Override + public void handleSuccess(GlobalSession globalSession) { + } + + @Override + public void handleStageCompleted(Stage currentState, + GlobalSession globalSession) { + } + + @Override + public boolean shouldBackOffStorage() { + return false; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java new file mode 100644 index 000000000..d8380df97 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.stage.AbstractNonRepositorySyncStage; + +public class MockAbstractNonRepositorySyncStage extends AbstractNonRepositorySyncStage { + @Override + public void execute() { + session.advance(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java new file mode 100644 index 000000000..f4af51f64 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.Utils; +import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; + +public class MockClientsDataDelegate implements ClientsDataDelegate { + private String accountGUID; + private String clientName; + private int clientsCount; + private long clientDataTimestamp = 0; + + @Override + public synchronized String getAccountGUID() { + if (accountGUID == null) { + accountGUID = Utils.generateGuid(); + } + return accountGUID; + } + + @Override + public synchronized String getDefaultClientName() { + return "Default client"; + } + + @Override + public synchronized void setClientName(String clientName, long now) { + this.clientName = clientName; + this.clientDataTimestamp = now; + } + + @Override + public synchronized String getClientName() { + if (clientName == null) { + setClientName(getDefaultClientName(), System.currentTimeMillis()); + } + return clientName; + } + + @Override + public synchronized void setClientsCount(int clientsCount) { + this.clientsCount = clientsCount; + } + + @Override + public synchronized int getClientsCount() { + return clientsCount; + } + + @Override + public synchronized boolean isLocalGUID(String guid) { + return getAccountGUID().equals(guid); + } + + @Override + public synchronized long getLastModifiedTimestamp() { + return clientDataTimestamp; + } + + @Override + public String getFormFactor() { + return "phone"; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java new file mode 100644 index 000000000..5e574e33d --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.mozilla.gecko.sync.CommandProcessor.Command; +import org.mozilla.gecko.sync.repositories.NullCursorException; +import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor; +import org.mozilla.gecko.sync.repositories.domain.ClientRecord; + +public class MockClientsDatabaseAccessor extends ClientsDatabaseAccessor { + public boolean storedRecord = false; + public boolean dbWiped = false; + public boolean clientsTableWiped = false; + public boolean closed = false; + public boolean storedArrayList = false; + public boolean storedCommand; + + @Override + public void store(ClientRecord record) { + storedRecord = true; + } + + @Override + public void store(Collection<ClientRecord> records) { + storedArrayList = false; + } + + @Override + public void store(String accountGUID, Command command) throws NullCursorException { + storedCommand = true; + } + + @Override + public ClientRecord fetchClient(String profileID) throws NullCursorException { + return null; + } + + @Override + public Map<String, ClientRecord> fetchAllClients() throws NullCursorException { + return null; + } + + @Override + public List<Command> fetchCommandsForClient(String accountGUID) throws NullCursorException { + return null; + } + + @Override + public int clientsCount() { + return 0; + } + + @Override + public void wipeDB() { + dbWiped = true; + } + + @Override + public void wipeClientsTable() { + clientsTableWiped = true; + } + + @Override + public void close() { + closed = true; + } + + public void resetVars() { + storedRecord = dbWiped = clientsTableWiped = closed = storedArrayList = false; + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java new file mode 100644 index 000000000..63afdd1ac --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.EngineSettings; +import org.mozilla.gecko.sync.NonObjectJSONException; +import org.mozilla.gecko.sync.SyncConfiguration; +import org.mozilla.gecko.sync.SyncConfigurationException; +import org.mozilla.gecko.sync.crypto.KeyBundle; +import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; +import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider; +import org.mozilla.gecko.sync.stage.CompletedStage; +import org.mozilla.gecko.sync.stage.GlobalSyncStage; +import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage; + +import java.io.IOException; +import java.util.HashMap; + + +public class MockGlobalSession extends MockPrefsGlobalSession { + + public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException { + this(new SyncConfiguration(username, new BasicAuthHeaderProvider(username, password), new MockSharedPreferences(), keyBundle), callback); + } + + public MockGlobalSession(SyncConfiguration config, GlobalSessionCallback callback) + throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException { + super(config, callback, null, null); + } + + @Override + public boolean isEngineRemotelyEnabled(String engine, EngineSettings engineSettings) { + return false; + } + + @Override + protected void prepareStages() { + super.prepareStages(); + HashMap<Stage, GlobalSyncStage> newStages = new HashMap<Stage, GlobalSyncStage>(this.stages); + + for (Stage stage : this.stages.keySet()) { + newStages.put(stage, new MockServerSyncStage()); + } + + // This signals that the global session is complete. + newStages.put(Stage.completed, new CompletedStage()); + + this.stages = newStages; + } + + public MockGlobalSession withStage(Stage stage, GlobalSyncStage syncStage) { + stages.put(stage, syncStage); + + return this; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java new file mode 100644 index 000000000..2ff29453f --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.mozilla.gecko.sync.GlobalSession; +import org.mozilla.gecko.sync.NonObjectJSONException; +import org.mozilla.gecko.sync.SyncConfiguration; +import org.mozilla.gecko.sync.SyncConfigurationException; +import org.mozilla.gecko.sync.crypto.KeyBundle; +import org.mozilla.gecko.sync.delegates.ClientsDataDelegate; +import org.mozilla.gecko.sync.delegates.GlobalSessionCallback; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider; + +import java.io.IOException; + +/** + * GlobalSession touches the Android prefs system. Stub that out. + */ +public class MockPrefsGlobalSession extends GlobalSession { + + public MockSharedPreferences prefs; + + public MockPrefsGlobalSession( + SyncConfiguration config, GlobalSessionCallback callback, Context context, + ClientsDataDelegate clientsDelegate) + throws SyncConfigurationException, IllegalArgumentException, IOException, + NonObjectJSONException { + super(config, callback, context, clientsDelegate); + } + + public static MockPrefsGlobalSession getSession( + String username, String password, + KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context, + ClientsDataDelegate clientsDelegate) + throws SyncConfigurationException, IllegalArgumentException, IOException, + NonObjectJSONException { + return getSession(username, new BasicAuthHeaderProvider(username, password), null, + syncKeyBundle, callback, context, clientsDelegate); + } + + public static MockPrefsGlobalSession getSession( + String username, AuthHeaderProvider authHeaderProvider, String prefsPath, + KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context, + ClientsDataDelegate clientsDelegate) + throws SyncConfigurationException, IllegalArgumentException, IOException, + NonObjectJSONException { + + final SharedPreferences prefs = new MockSharedPreferences(); + final SyncConfiguration config = new SyncConfiguration(username, authHeaderProvider, prefs); + config.syncKeyBundle = syncKeyBundle; + return new MockPrefsGlobalSession(config, callback, context, clientsDelegate); + } + + @Override + public Context getContext() { + return null; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockRecord.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockRecord.java new file mode 100644 index 000000000..d3a05bcec --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockRecord.java @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.repositories.domain.Record; + +public class MockRecord extends Record { + + public MockRecord(String guid, String collection, long lastModified, boolean deleted) { + super(guid, collection, lastModified, deleted); + } + + @Override + protected void populatePayload(ExtendedJSONObject payload) { + } + + @Override + protected void initFromPayload(ExtendedJSONObject payload) { + } + + @Override + public Record copyWithIDs(String guid, long androidID) { + MockRecord r = new MockRecord(guid, this.collection, this.lastModified, this.deleted); + r.androidID = androidID; + return r; + } + + @Override + public String toJSONString() { + return "{\"id\":\"" + guid + "\", \"payload\": \"foo\"}"; + } +}
\ No newline at end of file diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java new file mode 100644 index 000000000..02b72b676 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java @@ -0,0 +1,12 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + + +public class MockServerSyncStage extends BaseMockServerSyncStage { + @Override + public void execute() { + session.advance(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java new file mode 100644 index 000000000..bc49fa7fb --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * A programmable mock content provider. + */ +public class MockSharedPreferences implements SharedPreferences, SharedPreferences.Editor { + private HashMap<String, Object> mValues; + private HashMap<String, Object> mTempValues; + + public MockSharedPreferences() { + mValues = new HashMap<String, Object>(); + mTempValues = new HashMap<String, Object>(); + } + + public Editor edit() { + return this; + } + + public boolean contains(String key) { + return mValues.containsKey(key); + } + + public Map<String, ?> getAll() { + return new HashMap<String, Object>(mValues); + } + + public boolean getBoolean(String key, boolean defValue) { + if (mValues.containsKey(key)) { + return ((Boolean)mValues.get(key)).booleanValue(); + } + return defValue; + } + + public float getFloat(String key, float defValue) { + if (mValues.containsKey(key)) { + return ((Float)mValues.get(key)).floatValue(); + } + return defValue; + } + + public int getInt(String key, int defValue) { + if (mValues.containsKey(key)) { + return ((Integer)mValues.get(key)).intValue(); + } + return defValue; + } + + public long getLong(String key, long defValue) { + if (mValues.containsKey(key)) { + return ((Long)mValues.get(key)).longValue(); + } + return defValue; + } + + public String getString(String key, String defValue) { + if (mValues.containsKey(key)) + return (String)mValues.get(key); + return defValue; + } + + @SuppressWarnings("unchecked") + public Set<String> getStringSet(String key, Set<String> defValues) { + if (mValues.containsKey(key)) { + return (Set<String>) mValues.get(key); + } + return defValues; + } + + public void registerOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException(); + } + + public void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException(); + } + + public Editor putBoolean(String key, boolean value) { + mTempValues.put(key, Boolean.valueOf(value)); + return this; + } + + public Editor putFloat(String key, float value) { + mTempValues.put(key, value); + return this; + } + + public Editor putInt(String key, int value) { + mTempValues.put(key, value); + return this; + } + + public Editor putLong(String key, long value) { + mTempValues.put(key, value); + return this; + } + + public Editor putString(String key, String value) { + mTempValues.put(key, value); + return this; + } + + public Editor putStringSet(String key, Set<String> values) { + mTempValues.put(key, values); + return this; + } + + public Editor remove(String key) { + mTempValues.remove(key); + return this; + } + + public Editor clear() { + mTempValues.clear(); + return this; + } + + @SuppressWarnings("unchecked") + public boolean commit() { + mValues = (HashMap<String, Object>)mTempValues.clone(); + return true; + } + + public void apply() { + commit(); + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WBORepository.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WBORepository.java new file mode 100644 index 000000000..bd2e7f791 --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WBORepository.java @@ -0,0 +1,231 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.sync.repositories.InactiveSessionException; +import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException; +import org.mozilla.gecko.sync.repositories.NoStoreDelegateException; +import org.mozilla.gecko.sync.repositories.RecordFilter; +import org.mozilla.gecko.sync.repositories.Repository; +import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate; +import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate; +import org.mozilla.gecko.sync.repositories.domain.Record; + +import android.content.Context; + +public class WBORepository extends Repository { + + public class WBORepositoryStats { + public long created = -1; + public long begun = -1; + public long fetchBegan = -1; + public long fetchCompleted = -1; + public long storeBegan = -1; + public long storeCompleted = -1; + public long finished = -1; + } + + public static final String LOG_TAG = "WBORepository"; + + // Access to stats is not guarded. + public WBORepositoryStats stats; + + // Whether or not to increment the timestamp of stored records. + public final boolean bumpTimestamps; + + public class WBORepositorySession extends StoreTrackingRepositorySession { + + protected WBORepository wboRepository; + protected ExecutorService delegateExecutor = Executors.newSingleThreadExecutor(); + public ConcurrentHashMap<String, Record> wbos; + + public WBORepositorySession(WBORepository repository) { + super(repository); + + wboRepository = repository; + wbos = new ConcurrentHashMap<String, Record>(); + stats = new WBORepositoryStats(); + stats.created = now(); + } + + @Override + protected synchronized void trackGUID(String guid) { + if (wboRepository.shouldTrack()) { + super.trackGUID(guid); + } + } + + @Override + public void guidsSince(long timestamp, + RepositorySessionGuidsSinceDelegate delegate) { + throw new RuntimeException("guidsSince not implemented."); + } + + @Override + public void fetchSince(long timestamp, + RepositorySessionFetchRecordsDelegate delegate) { + long fetchBegan = now(); + stats.fetchBegan = fetchBegan; + RecordFilter filter = storeTracker.getFilter(); + + for (Entry<String, Record> entry : wbos.entrySet()) { + Record record = entry.getValue(); + if (record.lastModified >= timestamp) { + if (filter != null && + filter.excludeRecord(record)) { + Logger.debug(LOG_TAG, "Excluding record " + record.guid); + continue; + } + delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(record); + } + } + long fetchCompleted = now(); + stats.fetchCompleted = fetchCompleted; + delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted); + } + + @Override + public void fetch(final String[] guids, + final RepositorySessionFetchRecordsDelegate delegate) { + long fetchBegan = now(); + stats.fetchBegan = fetchBegan; + for (String guid : guids) { + if (wbos.containsKey(guid)) { + delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(wbos.get(guid)); + } + } + long fetchCompleted = now(); + stats.fetchCompleted = fetchCompleted; + delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted); + } + + @Override + public void fetchAll(final RepositorySessionFetchRecordsDelegate delegate) { + long fetchBegan = now(); + stats.fetchBegan = fetchBegan; + for (Entry<String, Record> entry : wbos.entrySet()) { + Record record = entry.getValue(); + delegate.deferredFetchDelegate(delegateExecutor).onFetchedRecord(record); + } + long fetchCompleted = now(); + stats.fetchCompleted = fetchCompleted; + delegate.deferredFetchDelegate(delegateExecutor).onFetchCompleted(fetchCompleted); + } + + @Override + public void store(final Record record) throws NoStoreDelegateException { + if (delegate == null) { + throw new NoStoreDelegateException(); + } + final long now = now(); + if (stats.storeBegan < 0) { + stats.storeBegan = now; + } + Record existing = wbos.get(record.guid); + Logger.debug(LOG_TAG, "Existing record is " + (existing == null ? "<null>" : (existing.guid + ", " + existing))); + if (existing != null && + existing.lastModified > record.lastModified) { + Logger.debug(LOG_TAG, "Local record is newer. Not storing."); + delegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid); + return; + } + if (existing != null) { + Logger.debug(LOG_TAG, "Replacing local record."); + } + + // Store a copy of the record with an updated modified time. + Record toStore = record.copyWithIDs(record.guid, record.androidID); + if (bumpTimestamps) { + toStore.lastModified = now; + } + wbos.put(record.guid, toStore); + + trackRecord(toStore); + delegate.deferredStoreDelegate(delegateExecutor).onRecordStoreSucceeded(record.guid); + } + + @Override + public void wipe(final RepositorySessionWipeDelegate delegate) { + if (!isActive()) { + delegate.onWipeFailed(new InactiveSessionException(null)); + return; + } + + Logger.info(LOG_TAG, "Wiping WBORepositorySession."); + this.wbos = new ConcurrentHashMap<String, Record>(); + + // Wipe immediately for the convenience of test code. + wboRepository.wbos = new ConcurrentHashMap<String, Record>(); + delegate.deferredWipeDelegate(delegateExecutor).onWipeSucceeded(); + } + + @Override + public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException { + Logger.info(LOG_TAG, "Finishing WBORepositorySession: handing back " + this.wbos.size() + " WBOs."); + wboRepository.wbos = this.wbos; + stats.finished = now(); + super.finish(delegate); + } + + @Override + public void begin(RepositorySessionBeginDelegate delegate) throws InvalidSessionTransitionException { + this.wbos = wboRepository.cloneWBOs(); + stats.begun = now(); + super.begin(delegate); + } + + @Override + public void storeDone(long end) { + // TODO: this is not guaranteed to be called after all of the record + // store callbacks have completed! + if (stats.storeBegan < 0) { + stats.storeBegan = end; + } + stats.storeCompleted = end; + delegate.deferredStoreDelegate(delegateExecutor).onStoreCompleted(end); + } + } + + public ConcurrentHashMap<String, Record> wbos; + + public WBORepository(boolean bumpTimestamps) { + super(); + this.bumpTimestamps = bumpTimestamps; + this.wbos = new ConcurrentHashMap<String, Record>(); + } + + public WBORepository() { + this(false); + } + + public synchronized boolean shouldTrack() { + return false; + } + + @Override + public void createSession(RepositorySessionCreationDelegate delegate, + Context context) { + delegate.deferredCreationDelegate().onSessionCreated(new WBORepositorySession(this)); + } + + public ConcurrentHashMap<String, Record> cloneWBOs() { + ConcurrentHashMap<String, Record> out = new ConcurrentHashMap<String, Record>(); + for (Entry<String, Record> entry : wbos.entrySet()) { + out.put(entry.getKey(), entry.getValue()); // Assume that records are + // immutable. + } + return out; + } +} diff --git a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java new file mode 100644 index 000000000..1a84977cf --- /dev/null +++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java @@ -0,0 +1,171 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.background.testhelpers; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.mozilla.gecko.background.common.log.Logger; + +/** + * Implements waiting for asynchronous test events. + * + * Call WaitHelper.getTestWaiter() to get the unique instance. + * + * Call performWait(runnable) to execute runnable synchronously. + * runnable *must* call performNotify() on all exit paths to signal to + * the TestWaiter that the runnable has completed. + * + * @author rnewman + * @author nalexander + */ +public class WaitHelper { + + public static final String LOG_TAG = "WaitHelper"; + + public static class Result { + public Throwable error; + public Result() { + error = null; + } + + public Result(Throwable error) { + this.error = error; + } + } + + public static abstract class WaitHelperError extends Error { + private static final long serialVersionUID = 7074690961681883619L; + } + + /** + * Immutable. + * + * @author rnewman + */ + public static class TimeoutError extends WaitHelperError { + private static final long serialVersionUID = 8591672555848651736L; + public final int waitTimeInMillis; + + public TimeoutError(int waitTimeInMillis) { + this.waitTimeInMillis = waitTimeInMillis; + } + } + + public static class MultipleNotificationsError extends WaitHelperError { + private static final long serialVersionUID = -9072736521571635495L; + } + + public static class InterruptedError extends WaitHelperError { + private static final long serialVersionUID = 8383948170038639308L; + } + + public static class InnerError extends WaitHelperError { + private static final long serialVersionUID = 3008502618576773778L; + public Throwable innerError; + + public InnerError(Throwable e) { + innerError = e; + if (e != null) { + // Eclipse prints the stack trace of the cause. + this.initCause(e); + } + } + } + + public BlockingQueue<Result> queue = new ArrayBlockingQueue<Result>(1); + + /** + * How long performWait should wait for, in milliseconds, with the + * convention that a negative value means "wait forever". + */ + public static int defaultWaitTimeoutInMillis = -1; + + public void performWait(Runnable action) throws WaitHelperError { + this.performWait(defaultWaitTimeoutInMillis, action); + } + + public void performWait(int waitTimeoutInMillis, Runnable action) throws WaitHelperError { + Logger.debug(LOG_TAG, "performWait called."); + + Result result = null; + + try { + if (action != null) { + try { + action.run(); + Logger.debug(LOG_TAG, "Action done."); + } catch (Exception ex) { + Logger.debug(LOG_TAG, "Performing action threw: " + ex.getMessage()); + throw new InnerError(ex); + } + } + + if (waitTimeoutInMillis < 0) { + result = queue.take(); + } else { + result = queue.poll(waitTimeoutInMillis, TimeUnit.MILLISECONDS); + } + Logger.debug(LOG_TAG, "Got result from queue: " + result); + } catch (InterruptedException e) { + // We were interrupted. + Logger.debug(LOG_TAG, "performNotify interrupted with InterruptedException " + e); + final InterruptedError interruptedError = new InterruptedError(); + interruptedError.initCause(e); + throw interruptedError; + } + + if (result == null) { + // We timed out. + throw new TimeoutError(waitTimeoutInMillis); + } else if (result.error != null) { + Logger.debug(LOG_TAG, "Notified with error: " + result.error.getMessage()); + + // Rethrow any assertion with which we were notified. + InnerError innerError = new InnerError(result.error); + throw innerError; + } + // Success! + } + + public void performNotify(final Throwable e) { + if (e != null) { + Logger.debug(LOG_TAG, "performNotify called with Throwable: " + e.getMessage()); + } else { + Logger.debug(LOG_TAG, "performNotify called."); + } + + if (!queue.offer(new Result(e))) { + // This could happen if performNotify is called multiple times (which is an error). + throw new MultipleNotificationsError(); + } + } + + public void performNotify() { + this.performNotify(null); + } + + public static Runnable onThreadRunnable(final Runnable r) { + return new Runnable() { + @Override + public void run() { + new Thread(r).start(); + } + }; + } + + private static WaitHelper singleWaiter = new WaitHelper(); + public static WaitHelper getTestWaiter() { + return singleWaiter; + } + + public static void resetTestWaiter() { + singleWaiter = new WaitHelper(); + } + + public boolean isIdle() { + return queue.isEmpty(); + } +} |