summaryrefslogtreecommitdiffstats
path: root/mobile/android/tests/background/junit3
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/tests/background/junit3')
-rw-r--r--mobile/android/tests/background/junit3/AndroidManifest.xml.in23
-rw-r--r--mobile/android/tests/background/junit3/Makefile.in13
-rw-r--r--mobile/android/tests/background/junit3/background_junit3_sources.mozbuild78
-rw-r--r--mobile/android/tests/background/junit3/instrumentation.ini22
-rw-r--r--mobile/android/tests/background/junit3/moz.build42
-rw-r--r--mobile/android/tests/background/junit3/res/drawable-hdpi/icon.pngbin0 -> 7639 bytes
-rw-r--r--mobile/android/tests/background/junit3/res/drawable-ldpi/icon.pngbin0 -> 2979 bytes
-rw-r--r--mobile/android/tests/background/junit3/res/drawable-mdpi/icon.pngbin0 -> 4625 bytes
-rw-r--r--mobile/android/tests/background/junit3/res/layout/main.xml12
-rw-r--r--mobile/android/tests/background/junit3/res/values/strings.xml6
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java68
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestUtils.java159
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/common/TestWaitHelper.java356
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java818
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java636
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java450
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestBookmarks.java1063
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabase.java200
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java128
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java297
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java441
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestPasswordsRepository.java482
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestTopSites.java92
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestAccountLoader.java163
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java49
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java134
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java52
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBHelpers.java84
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java73
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java175
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java128
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestClientsStage.java95
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java198
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestStoreTracking.java377
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java146
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestWebURLFinder.java49
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java216
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java33
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java21
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java52
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java106
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java60
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java19
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java53
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java71
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java22
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java16
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java32
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java47
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java17
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java15
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java41
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java24
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java18
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java48
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java33
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java11
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java17
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java39
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java90
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java94
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java82
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java20
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java18
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java22
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java20
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java20
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java69
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java40
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java52
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java13
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java66
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java76
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java57
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java63
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockRecord.java34
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java12
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java137
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WBORepository.java231
-rw-r--r--mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java171
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
new file mode 100644
index 000000000..e83438eee
--- /dev/null
+++ b/mobile/android/tests/background/junit3/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/mobile/android/tests/background/junit3/res/drawable-ldpi/icon.png b/mobile/android/tests/background/junit3/res/drawable-ldpi/icon.png
new file mode 100644
index 000000000..0483c95e9
--- /dev/null
+++ b/mobile/android/tests/background/junit3/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/mobile/android/tests/background/junit3/res/drawable-mdpi/icon.png b/mobile/android/tests/background/junit3/res/drawable-mdpi/icon.png
new file mode 100644
index 000000000..86b4dee54
--- /dev/null
+++ b/mobile/android/tests/background/junit3/res/drawable-mdpi/icon.png
Binary files differ
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();
+ }
+}