From 24261cfe8ffc1146123db1d8994292c1e0b9d0a9 Mon Sep 17 00:00:00 2001 From: Grisha Kruglov Date: Wed, 23 Oct 2019 11:59:38 -0700 Subject: [PATCH] [components] Fennec FxA state migration This adds basics of the Fennec Firefox Account state migration, bailing out if we're not processing v4 fxa state. Migration is done from the "pickle file". Cleaning up system account is TBD. --- .../components/support/migration/build.gradle | 3 + .../support/migration/FennecFxaMigration.kt | 284 +++++++++++++++++++ .../components/support/migration/FennecMigrator.kt | 105 ++++++- .../support/migration/FennecSessionMigration.kt | 2 +- .../support/migration/FennecFxaMigrationTest.kt | 308 +++++++++++++++++++++ .../support/migration/FennecMigratorTest.kt | 217 ++++++++++++++- .../src/test/resources/fxa/cohabiting-v4.json | 20 ++ .../src/test/resources/fxa/corrupt-married-v4.json | 20 ++ .../fxa/corrupt-separated-missing-versions-v4.json | 19 ++ .../src/test/resources/fxa/doghouse-v4.json | 20 ++ .../src/test/resources/fxa/married-v4.json | 20 ++ .../fxa/separated-bad-account-version-v4.json | 20 ++ .../fxa/separated-bad-pickle-version-v4.json | 20 ++ .../fxa/separated-bad-state-version-v10.json | 20 ++ .../test/resources/fxa/separated-bad-state.json | 20 ++ .../src/test/resources/fxa/separated-v4.json | 20 ++ 16 files changed, 1096 insertions(+), 22 deletions(-) create mode 100644 mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecFxaMigration.kt create mode 100644 mobile/android/android-components/components/support/migration/src/test/java/mozilla/components/support/migration/FennecFxaMigrationTest.kt create mode 100644 mobile/android/android-components/components/support/migration/src/test/resources/fxa/cohabiting-v4.json create mode 100644 mobile/android/android-components/components/support/migration/src/test/resources/fxa/corrupt-married-v4.json create mode 100644 mobile/android/android-components/components/support/migration/src/test/resources/fxa/corrupt-separated-missing-versions-v4.json create mode 100644 mobile/android/android-components/components/support/migration/src/test/resources/fxa/doghouse-v4.json create mode 100644 mobile/android/android-components/components/support/migration/src/test/resources/fxa/married-v4.json create mode 100644 mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-account-version-v4.json create mode 100644 mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-pickle-version-v4.json create mode 100644 mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-state-version-v10.json create mode 100644 mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-state.json create mode 100644 mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-v4.json diff --git a/mobile/android/android-components/components/support/migration/build.gradle b/mobile/android/android-components/components/support/migration/build.gradle index 418fffa2ce26..d84d951f8d9f 100644 --- a/mobile/android/android-components/components/support/migration/build.gradle +++ b/mobile/android/android-components/components/support/migration/build.gradle @@ -45,6 +45,9 @@ dependencies { implementation project(':browser-session') implementation project(':browser-storage-sync') + implementation project(':service-firefox-accounts') + + implementation project(':lib-crash') implementation Dependencies.kotlin_stdlib implementation Dependencies.kotlin_coroutines diff --git a/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecFxaMigration.kt b/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecFxaMigration.kt new file mode 100644 index 000000000000..77d134b1ba1d --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecFxaMigration.kt @@ -0,0 +1,284 @@ +/* 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/. */ + +package mozilla.components.support.migration + +import android.content.Context +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.sharing.ShareableAccount +import mozilla.components.service.fxa.sharing.ShareableAuthInfo +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.migration.FxaMigrationResult.Failure +import mozilla.components.support.migration.FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount +import org.json.JSONException +import org.json.JSONObject +import java.io.File + +/** + * A Fennec Firefox account. + */ +private data class FennecAccount(val email: String, val stateLabel: String, val authInfo: FennecAuthenticationInfo?) + +/** + * Encapsulates authentication info necessary to sign-in into a [FennecAccount]. + */ +private data class FennecAuthenticationInfo( + val sessionToken: String, + val kXCS: String, + val kSync: String +) { + override fun toString(): String { + return "Authenticated account" + } +} + +/** + * Wraps [FxaMigrationResult] in an exception so that it can be returned via [Result.Failure]. + * + * PII note - be careful to not log this exception, as it may contain personal information (wrapped in a JSONException). + * + * @property failure Wrapped [FxaMigrationResult] indicating exact failure reason. + */ +class FxaMigrationException(val failure: Failure) : Exception() + +/** + * Result of an FxA migration. + * + * Note that toString implementations are overridden to help avoid accidental logging of PII. + */ +sealed class FxaMigrationResult { + /** + * Success variants of an FxA migration. + */ + sealed class Success : FxaMigrationResult() { + /** + * No Fennec account found. + */ + object NoAccount : Success() + + /** + * Encountered a Fennec auth state that can't be used to automatically log-in. + */ + data class UnauthenticatedAccount(val email: String, val stateLabel: String) : Success() { + override fun toString(): String { + return "Unauthenticated account in state $stateLabel" + } + } + + /** + * Successfully signed-in with an authenticated Fennec account. + */ + data class SignedInIntoAuthenticatedAccount(val email: String, val stateLabel: String) : Success() { + override fun toString(): String { + return "Signed-in into an authenticated account" + } + } + } + + /** + * Failure variants of an FxA migration. + */ + sealed class Failure : FxaMigrationResult() { + /** + * Failed to process Fennec's auth state due to an exception [e]. + */ + data class CorruptAccountState(val e: JSONException) : Failure() { + override fun toString(): String { + return "Corrupt account state, exception type: ${e::class}" + } + } + + /** + * Encountered an unsupported version of Fennec's auth state. + */ + data class UnsupportedVersions( + val accountVersion: Int, + val pickleVersion: Int, + val stateVersion: Int? = null + ) : Failure() { + override fun toString(): String { + return "Unsupported version(s): account=$accountVersion, pickle=$pickleVersion, state=$stateVersion" + } + } + + /** + * Failed to sign in into an authenticated account. Currently, this could be either due to network failures, + * invalid credentials, or server-side issues. + */ + data class FailedToSignIntoAuthenticatedAccount(val email: String, val stateLabel: String) : Failure() + } +} + +/** + * Knows how to process an authenticated account detected during a migration. + */ +private object AuthenticatedAccountProcessor { + internal suspend fun signIn( + context: Context, + accountManager: FxaAccountManager, + fennecAccount: FennecAccount + ): Result { + require(fennecAccount.authInfo != null) { "authInfo must be present in order to sign-in a FennecAccount" } + + val fennecAuthInfo = ShareableAuthInfo( + sessionToken = fennecAccount.authInfo.sessionToken, + kSync = fennecAccount.authInfo.kSync, + kXCS = fennecAccount.authInfo.kXCS + ) + val shareableAccount = ShareableAccount( + email = fennecAccount.email, + sourcePackage = context.packageName, + authInfo = fennecAuthInfo + ) + + if (!accountManager.signInWithShareableAccountAsync(shareableAccount).await()) { + // What do we do now? + // We detected an account in a good state, and failed to actually login with it. + // This could indicate that credentials stored in Fennec are no longer valid, or that we had a + // network issue during the sign-in. + // Ideally, we're able to detect which one it is, and maybe retry in case of network issues. + // `signInWithShareableAccountAsync` needs to be expanded to support better error reporting. + return Result.Failure( + FxaMigrationException(Failure.FailedToSignIntoAuthenticatedAccount( + email = fennecAccount.email, + stateLabel = fennecAccount.stateLabel + )) + ) + } + return Result.Success(SignedInIntoAuthenticatedAccount( + email = fennecAccount.email, + stateLabel = fennecAccount.stateLabel + )) + } +} + +internal object FennecFxaMigration { + private val logger = Logger("FennecFxaMigration") + + /** + * Performs a migration of Fennec's FxA state located in [fxaStateFile]. + */ + suspend fun migrate( + fxaStateFile: File, + context: Context, + accountManager: FxaAccountManager + ): Result { + logger.debug("Migrating Fennec account at ${fxaStateFile.absolutePath}") + + if (!fxaStateFile.exists()) { + return Result.Success(FxaMigrationResult.Success.NoAccount) + } + + return try { + val fennecAccount = parseJSON(fxaStateFile.readText()) + + when (fennecAccount.authInfo) { + // Found an account, but it wasn't authenticated. + null -> Result.Success( + FxaMigrationResult.Success.UnauthenticatedAccount(fennecAccount.email, fennecAccount.stateLabel) + ) + // Found an authenticated account. Try to sign-in. + else -> AuthenticatedAccountProcessor.signIn(context, accountManager, fennecAccount) + } + } catch (e: FxaMigrationException) { + Result.Failure(e) + } + } + + @Suppress("MagicNumber", "ReturnCount", "ComplexMethod", "ThrowsCount") + @Throws(FxaMigrationException::class) + private fun parseJSON(rawJSON: String): FennecAccount { + val json = try { + JSONObject(rawJSON) + } catch (e: JSONException) { + logger.error("Corrupt FxA state: couldn't parse pickle file", e) + throw FxaMigrationException(Failure.CorruptAccountState(e)) + } + + val (accountVersion, pickleVersion) = try { + json.getInt("account_version") to json.getInt("pickle_version") + } catch (e: JSONException) { + logger.error("Corrupt FxA state: couldn't read versions", e) + throw FxaMigrationException(Failure.CorruptAccountState(e)) + } + + if (accountVersion != 3) { + // Account version changed from 2 to 3 in Firefox 29 (released spring of 2014). + // This is too long ago for us to worry about migrating older versions. + // https://bugzilla.mozilla.org/show_bug.cgi?id=962320 + throw FxaMigrationException(Failure.UnsupportedVersions(accountVersion, pickleVersion)) + } + + if (pickleVersion != 3) { + // Pickle version last changed in Firefox 38 (released winter of 2015). + // This is too long ago for us to worry about migrating older versions. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1123107 + throw FxaMigrationException(Failure.UnsupportedVersions(accountVersion, pickleVersion)) + } + + try { + val bundle = json.getJSONObject("bundle") + val state = JSONObject(bundle.getString("state")) + + // We support migrating versions 3 and 4 of the embedded "fxa state". In Fennec, this "state" represents + // a serialization of its state machine state. So, processing this state is the crux of our migration. + // Version 4 was introduced in Firefox 59 (released spring of 2018). + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1426305 + return when (val stateVersion = state.getInt("version")) { + // See https://github.com/mozilla-mobile/android-components/issues/4887 + // 3 -> processStateV3(state) + 4 -> processStateV4(bundle, state) + else -> throw FxaMigrationException( + Failure.UnsupportedVersions(accountVersion, pickleVersion, stateVersion) + ) + } + } catch (e: JSONException) { + logger.error("Corrupt FxA state: couldn't process state") + throw FxaMigrationException(Failure.CorruptAccountState(e)) + } + } + + @Throws(JSONException::class) + private fun processStateV4(bundle: JSONObject, state: JSONObject): FennecAccount { + val email = state.getString("email") + + // A state label represents in which Fennec FxA state machine state we find ourselves. + // "Cohabiting" and "Married" are the two states in which we have the derived keys, kXCS and kSync, + // which are necessary to decrypt sync data. + // "Married" state is the obvious terminal state of the state machine. + // "Cohabiting" state, as it relates to ability to authenticate new clients, + // is described in https://bugzilla.mozilla.org/show_bug.cgi?id=1568336. + // In essence, for regularly used Fennec instances with sync running at least once every 24hrs, we'll + // be in the "Married" state. Otherwise, it's likely we'll find ourselves in the "Cohabiting" state. + // NB: in both states, it's possible that our credentials are invalid, e.g. password could have been + // changed remotely since Fennec last synced. + val stateLabel = bundle.getString("stateLabel") + if (!listOf("Cohabiting", "Married").contains(stateLabel)) { + return FennecAccount(email = email, stateLabel = stateLabel, authInfo = null) + } + + val sessionToken = state.getString("sessionToken") + val kXCS = state.getString("kXCS") + val kSync = state.getString("kSync") + + return FennecAccount( + email = email, + stateLabel = stateLabel, + authInfo = FennecAuthenticationInfo(sessionToken = sessionToken, kSync = kSync, kXCS = kXCS) + ) + } + + // private fun processStateV3(state: JSONObject): FxaMigrationResult { + // See https://github.com/mozilla-mobile/android-components/issues/4887 where this is tracked. + + // In v3 fxa state, instead of storing derived kSync and kXCS, we stored kB itself. + // This method should: + // - read the kB + // - derive sync key + // - compute client state (kXCS) + // See similar conversion in Fennec: https://searchfox.org/mozilla-esr68/source/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/StateFactory.java#168-195 + // The rest of the state schema is the same as v4. + // logger.debug("Processing account state v3 $state") + // } +} diff --git a/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecMigrator.kt b/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecMigrator.kt index f41f34e39898..d6db0cbcbce4 100644 --- a/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecMigrator.kt +++ b/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecMigrator.kt @@ -14,11 +14,13 @@ import kotlinx.coroutines.async import mozilla.components.browser.session.SessionManager import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage +import mozilla.components.lib.crash.CrashReporter +import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.support.base.log.logger.Logger import java.io.File import java.lang.Exception -import java.lang.IllegalStateException import java.util.concurrent.Executors +import kotlin.IllegalStateException import kotlin.coroutines.CoroutineContext /** @@ -39,6 +41,11 @@ sealed class Migration(val currentVersion: Int) { * Migrates open tabs. */ object OpenTabs : Migration(currentVersion = 1) + + /** + * Migrates FxA state. + */ + object FxA : Migration(currentVersion = 1) } /** @@ -64,29 +71,35 @@ data class VersionedMigration(val migration: Migration, val version: Int = migra * @param bookmarksStorage An optional instance of [PlacesBookmarksStorage] used to store migrated bookmarks data. * @param coroutineContext An instance of [CoroutineContext] used for executing async migration tasks. */ +@Suppress("LargeClass") class FennecMigrator private constructor( private val context: Context, + private val crashReporter: CrashReporter, private val migrations: List, private val historyStorage: PlacesHistoryStorage?, private val bookmarksStorage: PlacesBookmarksStorage?, private val sessionManager: SessionManager?, + private val accountManager: FxaAccountManager?, private val profile: FennecProfile?, + private val fxaState: File?, private val browserDbName: String, private val coroutineContext: CoroutineContext ) { /** * Data migration builder. Allows configuring which migrations to run, their versions and relative order. */ - class Builder(private val context: Context) { + class Builder(private val context: Context, private val crashReporter: CrashReporter) { private var historyStorage: PlacesHistoryStorage? = null private var bookmarksStorage: PlacesBookmarksStorage? = null private var sessionManager: SessionManager? = null + private var accountManager: FxaAccountManager? = null private val migrations: MutableList = mutableListOf() // Single-thread executor to ensure we don't accidentally parallelize migrations. private var coroutineContext: CoroutineContext = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + private var fxaState = File("${context.filesDir}", "fxa.account.json") private var fennecProfile = FennecProfile.findDefault(context) private var browserDbName = "browser.db" @@ -97,6 +110,9 @@ class FennecMigrator private constructor( * @param version Version of the migration; defaults to the current version. */ fun migrateHistory(storage: PlacesHistoryStorage, version: Int = Migration.History.currentVersion): Builder { + check(migrations.find { it.migration is Migration.FxA } == null) { + "FxA migration, if desired, must run after history" + } historyStorage = storage migrations.add(VersionedMigration(Migration.History, version)) return this @@ -112,6 +128,9 @@ class FennecMigrator private constructor( storage: PlacesBookmarksStorage, version: Int = Migration.Bookmarks.currentVersion ): Builder { + check(migrations.find { it.migration is Migration.FxA } == null) { + "FxA migration, if desired, must run after bookmarks" + } check(migrations.find { it.migration is Migration.History } != null) { "To migrate bookmarks, you must first migrate history" } @@ -133,16 +152,31 @@ class FennecMigrator private constructor( } /** + * Enable FxA state migration. + * + * @param accountManager An instance of [FxaAccountManager] used for authenticating using a migrated account. + * @param version Version of the migration; defaults to the current version. + */ + fun migrateFxa(accountManager: FxaAccountManager, version: Int = Migration.FxA.currentVersion): Builder { + this.accountManager = accountManager + migrations.add(VersionedMigration(Migration.FxA, version)) + return this + } + + /** * Constructs a [FennecMigrator] based on the current configuration. */ fun build(): FennecMigrator { return FennecMigrator( context, + crashReporter, migrations, historyStorage, bookmarksStorage, sessionManager, + accountManager, fennecProfile, + fxaState, browserDbName, coroutineContext ) @@ -166,6 +200,12 @@ class FennecMigrator private constructor( fennecProfile = profile return this } + + @VisibleForTesting + internal fun setFxaState(state: File): Builder { + fxaState = state + return this + } } private val logger = Logger("FennecMigrator") @@ -207,16 +247,21 @@ class FennecMigrator private constructor( private fun runMigrationsAsync( migrations: List - ): Deferred = CoroutineScope(coroutineContext).async { synchronized(migrationLock) { + ): Deferred = CoroutineScope(coroutineContext).async { + + // Note that we're depending on coroutineContext to be backed by a single-threaded executor, in order to ensure + // non-overlapping execution of our migrations. + val results = mutableMapOf() migrations.forEach { versionedMigration -> - logger.debug("Migrating $versionedMigration") + logger.debug("Executing $versionedMigration") val migrationResult = when (versionedMigration.migration) { Migration.History -> migrateHistory() Migration.Bookmarks -> migrateBookmarks() Migration.OpenTabs -> migrateOpenTabs() + Migration.FxA -> migrateFxA() } results[versionedMigration.migration] = when (migrationResult) { @@ -239,14 +284,15 @@ class FennecMigrator private constructor( val migrationStore = MigrationResultsStore(context) migrationStore.setOrUpdate(results) results - } } + } @SuppressWarnings("TooGenericExceptionCaught") private fun migrateHistory(): Result { checkNotNull(historyStorage) { "History storage must be configured to migrate history" } if (dbPath == null) { - return Result.Failure(IllegalStateException("Missing DB path")) + crashReporter.submitCaughtException(IllegalStateException("Missing DB path during history migration")) + return Result.Failure(IllegalStateException("Missing DB path during history migration")) } return try { logger.debug("Migrating history...") @@ -263,7 +309,8 @@ class FennecMigrator private constructor( checkNotNull(bookmarksStorage) { "Bookmarks storage must be configured to migrate bookmarks" } if (dbPath == null) { - return Result.Failure(IllegalStateException("Missing DB path")) + crashReporter.submitCaughtException(IllegalStateException("Missing DB path during bookmark migration")) + return Result.Failure(IllegalStateException("Missing DB path during bookmark migration")) } return try { logger.debug("Migrating bookmarks...") @@ -278,6 +325,7 @@ class FennecMigrator private constructor( @SuppressWarnings("TooGenericExceptionCaught") private fun migrateOpenTabs(): Result { if (profile == null) { + crashReporter.submitCaughtException(IllegalStateException("Missing Profile path")) return Result.Failure(IllegalStateException("Missing Profile path")) } @@ -293,4 +341,47 @@ class FennecMigrator private constructor( Result.Failure(e) } } + + private suspend fun migrateFxA(): Result { + val result = FennecFxaMigration.migrate(fxaState!!, context, accountManager!!) + + if (result is Result.Failure) { + val migrationFailureWrapper = result.throwables.first() as FxaMigrationException + return when (val failure = migrationFailureWrapper.failure) { + is FxaMigrationResult.Failure.CorruptAccountState -> { + logger.error("Detected a corrupt account state: $failure") + result + } + is FxaMigrationResult.Failure.UnsupportedVersions -> { + logger.error("Detected unsupported versions: $failure") + result + } + is FxaMigrationResult.Failure.FailedToSignIntoAuthenticatedAccount -> { + logger.error("Failed to sign-in into an authenticated account") + crashReporter.submitCaughtException(migrationFailureWrapper) + result + } + } + } + + val migrationSuccess = result as Result.Success + return when (migrationSuccess.value as FxaMigrationResult.Success) { + // The rest are all successful migrations. + is FxaMigrationResult.Success.NoAccount -> { + logger.debug("No Fennec account detected") + result + } + is FxaMigrationResult.Success.UnauthenticatedAccount -> { + // Here we have an 'email' and a state label. + // "Bad auth state" could be a few things - unverified account, bad credentials detected by Fennec, etc. + // We could try using the 'email' address as a starting point in the authentication flow. + logger.debug("Detected a Fennec account in a bad authentication state: migrationResult") + result + } + is FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount -> { + logger.debug("Signed-in into a detected Fennec account") + result + } + } + } } diff --git a/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecSessionMigration.kt b/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecSessionMigration.kt index 0b22356da80e..c06aeb9fd3d6 100644 --- a/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecSessionMigration.kt +++ b/mobile/android/android-components/components/support/migration/src/main/java/mozilla/components/support/migration/FennecSessionMigration.kt @@ -17,7 +17,7 @@ import java.lang.Exception /** * Helper for importing/migrating "open tabs" from Fennec. */ -object FennecSessionMigration { +internal object FennecSessionMigration { private val logger = Logger("FennecSessionImporter") /** diff --git a/mobile/android/android-components/components/support/migration/src/test/java/mozilla/components/support/migration/FennecFxaMigrationTest.kt b/mobile/android/android-components/components/support/migration/src/test/java/mozilla/components/support/migration/FennecFxaMigrationTest.kt new file mode 100644 index 000000000000..709df8319128 --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/java/mozilla/components/support/migration/FennecFxaMigrationTest.kt @@ -0,0 +1,308 @@ +/* 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/. */ + +package mozilla.components.support.migration + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking +import mozilla.components.service.fxa.manager.FxaAccountManager +import mozilla.components.service.fxa.sharing.ShareableAccount +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.json.JSONException +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions +import java.io.File + +@RunWith(AndroidJUnit4::class) +class FennecFxaMigrationTest { + @Test + fun `no state`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "no-file.json") + val accountManager: FxaAccountManager = mock() + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Success) { + assertTrue(this.value is FxaMigrationResult.Success.NoAccount) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `separated fxa state v4`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "separated-v4.json") + val accountManager: FxaAccountManager = mock() + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Success) { + assertEquals(FxaMigrationResult.Success.UnauthenticatedAccount::class, this.value::class) + + val state = this.value as FxaMigrationResult.Success.UnauthenticatedAccount + + assertEquals("test@example.com", state.email) + assertEquals("Separated", state.stateLabel) + assertEquals("Unauthenticated account in state Separated", state.toString()) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `doghouse fxa state v4`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "doghouse-v4.json") + val accountManager: FxaAccountManager = mock() + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Success) { + assertEquals(FxaMigrationResult.Success.UnauthenticatedAccount::class, this.value::class) + + val state = this.value as FxaMigrationResult.Success.UnauthenticatedAccount + + assertEquals("test@example.com", state.email) + assertEquals("Doghouse", state.stateLabel) + assertEquals("Unauthenticated account in state Doghouse", state.toString()) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `married fxa state v4 successfull sign-in`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "married-v4.json") + val accountManager: FxaAccountManager = mock() + + `when`(accountManager.signInWithShareableAccountAsync(any())).thenReturn(CompletableDeferred(true)) + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Success) { + assertEquals(FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount::class, this.value::class) + assertEquals("test@example.com", (this.value as FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount).email) + assertEquals("Married", (this.value as FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount).stateLabel) + + val captor = argumentCaptor() + verify(accountManager).signInWithShareableAccountAsync(captor.capture()) + + assertEquals("test@example.com", captor.value.email) + assertEquals("252fsvj8932vj32movj97325hjfksdhfjstrg23yurt267r23", captor.value.authInfo.kSync) + assertEquals("0b3ba79bfxdf32f3of32jowef7987f", captor.value.authInfo.kXCS) + assertEquals("fjsdkfksf3e8f32f23f832fwf32jf89o327u2843gj23", captor.value.authInfo.sessionToken) + } + } + + @Test + fun `cohabiting fxa state v4 successful sign-in`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "cohabiting-v4.json") + val accountManager: FxaAccountManager = mock() + + `when`(accountManager.signInWithShareableAccountAsync(any())).thenReturn(CompletableDeferred(true)) + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Success) { + assertEquals(FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount::class, this.value::class) + assertEquals("test@example.com", (this.value as FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount).email) + assertEquals("Cohabiting", (this.value as FxaMigrationResult.Success.SignedInIntoAuthenticatedAccount).stateLabel) + + val captor = argumentCaptor() + verify(accountManager).signInWithShareableAccountAsync(captor.capture()) + + assertEquals("test@example.com", captor.value.email) + assertEquals("252bc4ccc3a239fsdfsdf32fg32wf3w4e3472d41d1a204890", captor.value.authInfo.kSync) + assertEquals("0b3ba79b18bd9fsdfsdf4g234adedd87", captor.value.authInfo.kXCS) + assertEquals("fsdfjsdffsdf342f23g3ogou97328uo23ij", captor.value.authInfo.sessionToken) + } + } + + @Test + fun `married fxa state v4 failed sign-in`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "married-v4.json") + val accountManager: FxaAccountManager = mock() + + `when`(accountManager.signInWithShareableAccountAsync(any())).thenReturn(CompletableDeferred(false)) + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Failure) { + val unwrapped = this.throwables.first() as FxaMigrationException + assertEquals(FxaMigrationResult.Failure.FailedToSignIntoAuthenticatedAccount::class, unwrapped.failure::class) + val unwrappedFailure = unwrapped.failure as FxaMigrationResult.Failure.FailedToSignIntoAuthenticatedAccount + assertEquals("test@example.com", unwrappedFailure.email) + assertEquals("Married", (unwrapped.failure as FxaMigrationResult.Failure.FailedToSignIntoAuthenticatedAccount).stateLabel) + + val captor = argumentCaptor() + verify(accountManager).signInWithShareableAccountAsync(captor.capture()) + + assertEquals("test@example.com", captor.value.email) + assertEquals("252fsvj8932vj32movj97325hjfksdhfjstrg23yurt267r23", captor.value.authInfo.kSync) + assertEquals("0b3ba79bfxdf32f3of32jowef7987f", captor.value.authInfo.kXCS) + assertEquals("fjsdkfksf3e8f32f23f832fwf32jf89o327u2843gj23", captor.value.authInfo.sessionToken) + } + } + + @Test + fun `cohabiting fxa state v4 failed sign-in`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "cohabiting-v4.json") + val accountManager: FxaAccountManager = mock() + + `when`(accountManager.signInWithShareableAccountAsync(any())).thenReturn(CompletableDeferred(false)) + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Failure) { + val unwrapped = this.throwables.first() as FxaMigrationException + assertEquals(FxaMigrationResult.Failure.FailedToSignIntoAuthenticatedAccount::class, unwrapped.failure::class) + val unwrappedFailure = unwrapped.failure as FxaMigrationResult.Failure.FailedToSignIntoAuthenticatedAccount + assertEquals("test@example.com", unwrappedFailure.email) + assertEquals("Cohabiting", unwrappedFailure.stateLabel) + + val captor = argumentCaptor() + verify(accountManager).signInWithShareableAccountAsync(captor.capture()) + + assertEquals("test@example.com", captor.value.email) + assertEquals("252bc4ccc3a239fsdfsdf32fg32wf3w4e3472d41d1a204890", captor.value.authInfo.kSync) + assertEquals("0b3ba79b18bd9fsdfsdf4g234adedd87", captor.value.authInfo.kXCS) + assertEquals("fsdfjsdffsdf342f23g3ogou97328uo23ij", captor.value.authInfo.sessionToken) + } + } + + @Test + fun `corrupt married fxa state v4`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "corrupt-married-v4.json") + val accountManager: FxaAccountManager = mock() + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Failure) { + assertEquals(1, this.throwables.size) + val fxaMigrationException = this.throwables.first() + assertEquals(FxaMigrationException::class, fxaMigrationException::class) + assertEquals( + FxaMigrationResult.Failure.CorruptAccountState::class, + (fxaMigrationException as FxaMigrationException).failure::class + ) + val corruptAccountResult = fxaMigrationException.failure as FxaMigrationResult.Failure.CorruptAccountState + assertEquals(JSONException::class, corruptAccountResult.e::class) + + assertEquals("Corrupt account state, exception type: class org.json.JSONException", corruptAccountResult.toString()) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `corrupt missing versions fxa state v4`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "corrupt-separated-missing-versions-v4.json") + val accountManager: FxaAccountManager = mock() + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Failure) { + assertEquals(1, this.throwables.size) + val fxaMigrationException = this.throwables.first() + assertEquals(FxaMigrationException::class, fxaMigrationException::class) + assertEquals( + FxaMigrationResult.Failure.CorruptAccountState::class, + (fxaMigrationException as FxaMigrationException).failure::class + ) + val corruptAccountResult = fxaMigrationException.failure as FxaMigrationResult.Failure.CorruptAccountState + assertEquals(JSONException::class, corruptAccountResult.e::class) + + assertEquals("Corrupt account state, exception type: class org.json.JSONException", corruptAccountResult.toString()) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `corrupt bad fxa state`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "separated-bad-state.json") + val accountManager: FxaAccountManager = mock() + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Failure) { + assertEquals(1, this.throwables.size) + val fxaMigrationException = this.throwables.first() + assertEquals(FxaMigrationException::class, fxaMigrationException::class) + assertEquals( + FxaMigrationResult.Failure.CorruptAccountState::class, + (fxaMigrationException as FxaMigrationException).failure::class + ) + val corruptAccountResult = fxaMigrationException.failure as FxaMigrationResult.Failure.CorruptAccountState + assertEquals(JSONException::class, corruptAccountResult.e::class) + + assertEquals("Corrupt account state, exception type: class org.json.JSONException", corruptAccountResult.toString()) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `separated - bad account version`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "separated-bad-account-version-v4.json") + val accountManager: FxaAccountManager = mock() + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Failure) { + assertEquals(1, this.throwables.size) + val fxaMigrationException = this.throwables.first() + assertEquals(FxaMigrationException::class, fxaMigrationException::class) + assertEquals( + FxaMigrationResult.Failure.UnsupportedVersions::class, + (fxaMigrationException as FxaMigrationException).failure::class + ) + val unsupportedVersionsResult = fxaMigrationException.failure as FxaMigrationResult.Failure.UnsupportedVersions + assertEquals(23, unsupportedVersionsResult.accountVersion) + assertEquals(3, unsupportedVersionsResult.pickleVersion) + // We didn't process state after detecting bad account version. + assertNull(unsupportedVersionsResult.stateVersion) + + assertEquals("Unsupported version(s): account=23, pickle=3, state=null", unsupportedVersionsResult.toString()) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `separated - bad pickle version`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "separated-bad-pickle-version-v4.json") + val accountManager: FxaAccountManager = mock() + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Failure) { + assertEquals(1, this.throwables.size) + val fxaMigrationException = this.throwables.first() + assertEquals(FxaMigrationException::class, fxaMigrationException::class) + assertEquals( + FxaMigrationResult.Failure.UnsupportedVersions::class, + (fxaMigrationException as FxaMigrationException).failure::class + ) + val unsupportedVersionsResult = fxaMigrationException.failure as FxaMigrationResult.Failure.UnsupportedVersions + assertEquals(3, unsupportedVersionsResult.accountVersion) + assertEquals(34, unsupportedVersionsResult.pickleVersion) + // We didn't process state after detecting bad account version. + assertNull(unsupportedVersionsResult.stateVersion) + + assertEquals("Unsupported version(s): account=3, pickle=34, state=null", unsupportedVersionsResult.toString()) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `separated - bad state version`() = runBlocking { + val fxaPath = File(getTestPath("fxa"), "separated-bad-state-version-v10.json") + val accountManager: FxaAccountManager = mock() + + with(FennecFxaMigration.migrate(fxaPath, testContext, accountManager) as Result.Failure) { + assertEquals(1, this.throwables.size) + val fxaMigrationException = this.throwables.first() + assertEquals(FxaMigrationException::class, fxaMigrationException::class) + assertEquals( + FxaMigrationResult.Failure.UnsupportedVersions::class, + (fxaMigrationException as FxaMigrationException).failure::class + ) + val unsupportedVersionsResult = fxaMigrationException.failure as FxaMigrationResult.Failure.UnsupportedVersions + assertEquals(3, unsupportedVersionsResult.accountVersion) + assertEquals(3, unsupportedVersionsResult.pickleVersion) + assertEquals(10, unsupportedVersionsResult.stateVersion) + + assertEquals("Unsupported version(s): account=3, pickle=3, state=10", unsupportedVersionsResult.toString()) + } + + verifyZeroInteractions(accountManager) + } +} \ No newline at end of file diff --git a/mobile/android/android-components/components/support/migration/src/test/java/mozilla/components/support/migration/FennecMigratorTest.kt b/mobile/android/android-components/components/support/migration/src/test/java/mozilla/components/support/migration/FennecMigratorTest.kt index e65b32d2a30d..a359e3f7479f 100644 --- a/mobile/android/android-components/components/support/migration/src/test/java/mozilla/components/support/migration/FennecMigratorTest.kt +++ b/mobile/android/android-components/components/support/migration/src/test/java/mozilla/components/support/migration/FennecMigratorTest.kt @@ -10,6 +10,7 @@ import mozilla.appservices.places.PlacesException import mozilla.components.browser.session.SessionManager import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage +import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.support.test.any import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext @@ -23,14 +24,18 @@ import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions import java.io.File import java.lang.IllegalStateException +import kotlinx.coroutines.CompletableDeferred +import mozilla.components.service.fxa.sharing.ShareableAccount +import mozilla.components.support.test.argumentCaptor @RunWith(AndroidJUnit4::class) class FennecMigratorTest { @Test fun `no-op migration`() = runBlocking { - val migrator = FennecMigrator.Builder(testContext) + val migrator = FennecMigrator.Builder(testContext, mock()) .setCoroutineContext(this.coroutineContext) .build() @@ -46,18 +51,16 @@ class FennecMigratorTest { } @Test - fun `history migration must be done before bookmarks`() = runBlocking { + fun `history migration must be done before bookmarks`() { try { - FennecMigrator.Builder(testContext) - .setCoroutineContext(this.coroutineContext) + FennecMigrator.Builder(testContext, mock()) .migrateBookmarks(mock()) .build() fail() } catch (e: IllegalStateException) {} try { - FennecMigrator.Builder(testContext) - .setCoroutineContext(this.coroutineContext) + FennecMigrator.Builder(testContext, mock()) .migrateBookmarks(mock()) .migrateHistory(mock()) .build() @@ -66,11 +69,48 @@ class FennecMigratorTest { } @Test + fun `history & bookmark migrations must be done before FxA`() { + try { + FennecMigrator.Builder(testContext, mock()) + .migrateFxa(mock()) + .migrateHistory(mock()) + .build() + fail() + } catch (e: IllegalStateException) {} + + try { + FennecMigrator.Builder(testContext, mock()) + .migrateFxa(mock()) + .migrateBookmarks(mock()) + .build() + fail() + } catch (e: IllegalStateException) {} + + try { + FennecMigrator.Builder(testContext, mock()) + .migrateFxa(mock()) + .migrateHistory(mock()) + .migrateBookmarks(mock()) + .build() + fail() + } catch (e: IllegalStateException) {} + + try { + FennecMigrator.Builder(testContext, mock()) + .migrateHistory(mock()) + .migrateFxa(mock()) + .migrateBookmarks(mock()) + .build() + fail() + } catch (e: IllegalStateException) {} + } + + @Test fun `migrations versioning basics`() = runBlocking { val historyStore = PlacesHistoryStorage(testContext) val bookmarksStore = PlacesBookmarksStorage(testContext) - val migrator = FennecMigrator.Builder(testContext) + val migrator = FennecMigrator.Builder(testContext, mock()) .setCoroutineContext(this.coroutineContext) .setProfile(FennecProfile( "test", File(getTestPath("combined"), "basic").absolutePath, true) @@ -114,7 +154,7 @@ class FennecMigratorTest { // Can add another migration type, and it will be the only one to run. val sessionManager: SessionManager = mock() - val expandedMigrator = FennecMigrator.Builder(testContext) + val expandedMigrator = FennecMigrator.Builder(testContext, mock()) .setCoroutineContext(this.coroutineContext) .setProfile(FennecProfile( "test", @@ -151,7 +191,7 @@ class FennecMigratorTest { val historyStorage: PlacesHistoryStorage = mock() // DB path/profile are not configured correctly for the test environment - val migrator = FennecMigrator.Builder(testContext) + val migrator = FennecMigrator.Builder(testContext, mock()) .migrateHistory(historyStorage) .setCoroutineContext(this.coroutineContext) .build() @@ -175,7 +215,7 @@ class FennecMigratorTest { // Fail during history migration. `when`(historyStorage.importFromFennec(any())).thenThrow(PlacesException("test exception")) - val migrator = FennecMigrator.Builder(testContext) + val migrator = FennecMigrator.Builder(testContext, mock()) .migrateHistory(historyStorage) .setCoroutineContext(this.coroutineContext) .setProfile(FennecProfile( @@ -205,7 +245,7 @@ class FennecMigratorTest { val historyStorage: PlacesHistoryStorage = mock() // DB path missing. - val migrator = FennecMigrator.Builder(testContext) + val migrator = FennecMigrator.Builder(testContext, mock()) .migrateHistory(historyStorage) .migrateBookmarks(bookmarkStorage) .setCoroutineContext(this.coroutineContext) @@ -234,7 +274,7 @@ class FennecMigratorTest { `when`(historyStorage.importFromFennec(any())).thenThrow(PlacesException("test exception")) // DB path is configured, partial success (only history failed). - val migrator = FennecMigrator.Builder(testContext) + val migrator = FennecMigrator.Builder(testContext, mock()) .migrateHistory(historyStorage) .migrateBookmarks(bookmarkStorage) .setCoroutineContext(this.coroutineContext) @@ -270,7 +310,7 @@ class FennecMigratorTest { `when`(historyStorage.importFromFennec(any())).thenThrow(PlacesException("test exception")) `when`(bookmarkStorage.importFromFennec(any())).thenThrow(PlacesException("test exception")) - val migrator = FennecMigrator.Builder(testContext) + val migrator = FennecMigrator.Builder(testContext, mock()) .migrateHistory(historyStorage) .migrateBookmarks(bookmarkStorage) .setCoroutineContext(this.coroutineContext) @@ -300,8 +340,9 @@ class FennecMigratorTest { @Test fun `failing migrations are reported - case 6`() = runBlocking { // Open tabs migration without configured path to sessions. - val migrator = FennecMigrator.Builder(testContext) + val migrator = FennecMigrator.Builder(testContext, mock()) .migrateOpenTabs(mock()) + .setCoroutineContext(this.coroutineContext) .build() with(migrator.migrateAsync().await()) { @@ -310,4 +351,152 @@ class FennecMigratorTest { assertFalse(this.getValue(Migration.OpenTabs).success) } } + + @Test + fun `failing migrations are reported - case 7, corrupt fxa state`() = runBlocking { + val accountManager: FxaAccountManager = mock() + val migrator = FennecMigrator.Builder(testContext, mock()) + .migrateFxa(accountManager) + .setFxaState(File(getTestPath("fxa"), "corrupt-married-v4.json")) + .setCoroutineContext(this.coroutineContext) + .build() + + with(migrator.migrateAsync().await()) { + assertEquals(1, this.size) + assertTrue(this.containsKey(Migration.FxA)) + assertFalse(this.getValue(Migration.FxA).success) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `failing migrations are reported - case 8, unsupported pickle version`() = runBlocking { + val accountManager: FxaAccountManager = mock() + val migrator = FennecMigrator.Builder(testContext, mock()) + .migrateFxa(accountManager) + .setFxaState(File(getTestPath("fxa"), "separated-bad-pickle-version-v4.json")) + .setCoroutineContext(this.coroutineContext) + .build() + + with(migrator.migrateAsync().await()) { + assertEquals(1, this.size) + assertTrue(this.containsKey(Migration.FxA)) + assertFalse(this.getValue(Migration.FxA).success) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `failing migrations are reported - case 8, unsupported state version`() = runBlocking { + val accountManager: FxaAccountManager = mock() + val migrator = FennecMigrator.Builder(testContext, mock()) + .migrateFxa(accountManager) + .setFxaState(File(getTestPath("fxa"), "separated-bad-state-version-v10.json")) + .setCoroutineContext(this.coroutineContext) + .build() + + with(migrator.migrateAsync().await()) { + assertEquals(1, this.size) + assertTrue(this.containsKey(Migration.FxA)) + assertFalse(this.getValue(Migration.FxA).success) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `fxa migration - no account`() = runBlocking { + // FxA migration without configured path to pickle file (test environment path isn't the same as real path). + val accountManager: FxaAccountManager = mock() + val migrator = FennecMigrator.Builder(testContext, mock()) + .migrateFxa(accountManager) + .setCoroutineContext(this.coroutineContext) + .build() + + with(migrator.migrateAsync().await()) { + assertEquals(1, this.size) + assertTrue(this.containsKey(Migration.FxA)) + assertTrue(this.getValue(Migration.FxA).success) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `fxa migration - unauthenticated account`() = runBlocking { + // FxA migration without configured path to pickle file (test environment path isn't the same as real path). + val accountManager: FxaAccountManager = mock() + val migrator = FennecMigrator.Builder(testContext, mock()) + .migrateFxa(accountManager) + .setFxaState(File(getTestPath("fxa"), "separated-v4.json")) + .setCoroutineContext(this.coroutineContext) + .build() + + with(migrator.migrateAsync().await()) { + assertEquals(1, this.size) + assertTrue(this.containsKey(Migration.FxA)) + assertTrue(this.getValue(Migration.FxA).success) + } + + verifyZeroInteractions(accountManager) + } + + @Test + fun `fxa migration - authenticated account, sign-in succeeded`() = runBlocking { + val accountManager: FxaAccountManager = mock() + val migrator = FennecMigrator.Builder(testContext, mock()) + .migrateFxa(accountManager) + .setFxaState(File(getTestPath("fxa"), "married-v4.json")) + .setCoroutineContext(this.coroutineContext) + .build() + + `when`(accountManager.signInWithShareableAccountAsync(any())).thenReturn(CompletableDeferred(true)) + + with(migrator.migrateAsync().await()) { + assertEquals(1, this.size) + assertTrue(this.containsKey(Migration.FxA)) + assertTrue(this.getValue(Migration.FxA).success) + } + + val captor = argumentCaptor() + verify(accountManager).signInWithShareableAccountAsync(captor.capture()) + + assertEquals("test@example.com", captor.value.email) + // This is going to be package name (org.mozilla.firefox) in actual builds. + assertEquals("mozilla.components.support.migration.test", captor.value.sourcePackage) + assertEquals("252fsvj8932vj32movj97325hjfksdhfjstrg23yurt267r23", captor.value.authInfo.kSync) + assertEquals("0b3ba79bfxdf32f3of32jowef7987f", captor.value.authInfo.kXCS) + assertEquals("fjsdkfksf3e8f32f23f832fwf32jf89o327u2843gj23", captor.value.authInfo.sessionToken) + } + + @Test + fun `fxa migration - authenticated account, sign-in failed`() = runBlocking { + val accountManager: FxaAccountManager = mock() + val migrator = FennecMigrator.Builder(testContext, mock()) + .migrateFxa(accountManager) + .setFxaState(File(getTestPath("fxa"), "cohabiting-v4.json")) + .setCoroutineContext(this.coroutineContext) + .build() + + // For now, we don't treat sign-in failure any different from success. E.g. it's a one-shot attempt. + `when`(accountManager.signInWithShareableAccountAsync(any())).thenReturn(CompletableDeferred(false)) + + with(migrator.migrateAsync().await()) { + assertEquals(1, this.size) + assertTrue(this.containsKey(Migration.FxA)) + assertFalse(this.getValue(Migration.FxA).success) + } + + val captor = argumentCaptor() + verify(accountManager).signInWithShareableAccountAsync(captor.capture()) + + assertEquals("test@example.com", captor.value.email) + // This is going to be package name (org.mozilla.firefox) in actual builds. + assertEquals("mozilla.components.support.migration.test", captor.value.sourcePackage) + assertEquals("252bc4ccc3a239fsdfsdf32fg32wf3w4e3472d41d1a204890", captor.value.authInfo.kSync) + assertEquals("0b3ba79b18bd9fsdfsdf4g234adedd87", captor.value.authInfo.kXCS) + assertEquals("fsdfjsdffsdf342f23g3ogou97328uo23ij", captor.value.authInfo.sessionToken) + } } \ No newline at end of file diff --git a/mobile/android/android-components/components/support/migration/src/test/resources/fxa/cohabiting-v4.json b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/cohabiting-v4.json new file mode 100644 index 000000000000..2402de93894e --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/cohabiting-v4.json @@ -0,0 +1,20 @@ +{ + "account_type":"org.mozilla.firefox_fxaccount", + "authoritiesToSyncAutomaticallyMap":{ + "org.mozilla.firefox.db.browser":true + }, + "account_version":3, + "profileServerURI":"https:\/\/profile.accounts.firefox.com\/v1", + "idpServerURI":"https:\/\/api.accounts.firefox.com\/v1", + "tokenServerURI":"https:\/\/token.services.mozilla.com\/1.0\/sync\/1.5", + "profile":"default", + "pickle_version":3, + "bundle":{ + "profile":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"displayName\":\"grisha\",\"amrValues\":[\"pwd\",\"email\"],\"twoFactorAuthentication\":false,\"avatar\":\"https:\\\/\\\/firefoxusercontent.com\\\/76a0f7cac96ecd290fef3fdsf42g32286\",\"locale\":\"en-US,en-CA;q=0.7,en;q=0.3\",\"email\":\"test@example.com\",\"avatarDefault\":false}", + "stateLabel":"Cohabiting", + "state":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"verified\":true,\"sessionToken\":\"fsdfjsdffsdf342f23g3ogou97328uo23ij\",\"keyPair\":{\"privateKey\":{\"p\":\"fe4125e6cbfe6dd5a410a8b91629451f78ff96ee7f404db5d48ebfc453e265518c3b117ea8851ec0d1b8f2042466f80be4ae7daefe076949510d7f483250b9b59c66827d70688ffae06e0ad9d1db982f2a91ca03152f333480aea5894c25afsdfg32g32g32sdfsd386a939ef3a5cfcaf7942c92ce18e6701a7a92fc197195031\",\"q\":\"acc074d502b2de6b7be9cff4f77b6ca25133651d\",\"g\":\"3caeec35fd9d8f0ed468ac181a8ac1b1aed1bae6ed5bca39ecca3f359dd71fef059f59129da02ec0317409b2c6a4f0cea1e83aa9cdb54a395665ffad9c329f0b556412968c4d132076661c2d5fc67d9616de1ca22a214aac7dbdba6f0c23d309427f356ac0cc1608fbd772267ef060c4ad0f289aa3a1470abffa045fa7847bf6\",\"x\":\"5bf6bc179e0aa092b1c9f5816881d5b1a20f084b\",\"algorithm\":\"DS\"},\"publicKey\":{\"p\":\"fe4125e6cbfe6dd5a410a8b91629451f78ff96ee7f404db5d48ebfc453e265518c3b117ea8851ec0d1b8f2042466f80be4ae7daefe076949510d7f483250b9b59c66827d70688ffae06e0ad9d1db982f2a91ca03152f333480aea5894c25a132753d62cde8c92e7386a939ef3a5cfcaf7942c92ce18e6701a7a92fc197195031\",\"q\":\"acc074d502b2de6b7be9cff4f77b6ca25133651d\",\"g\":\"3caeec35fd9d8f0ed468ac181a8ac1b1aed1bae6ed5bca39ecca3f359dd71fef059f59129da02ec0317409b2c6a4f0cea1e83aa9cdb54a395665ffad9c329f0b556412968c4d132076661c2d5fc67d9616de1ca22a214aac7dbdba6f0c23d309427f356ac0cc1608fbd772267ef060c4ad0f289aa3a1470abffa045fa7847bf6\",\"y\":\"a06e893294fdcd5caa7b7e5b381d6aba783f53cf69d667b7d64d86808be11fe58569bcbbe9f1fb1087832f21db0f317d2073c180605c53b1fe9474142ac582e03b0280e5bc80b8f9f468d7e2a13183e0e9f79e0644388ccb629d840d8cb4f17a3e34931416fe3ac7fc073f4a003ab68088971474674dd232088457f8dc92507e\",\"algorithm\":\"DS\"}},\"kXCS\":\"0b3ba79b18bd9fsdfsdf4g234adedd87\",\"version\":4,\"kSync\":\"252bc4ccc3a239fsdfsdf32fg32wf3w4e3472d41d1a204890\",\"email\":\"test@example.com\"}", + "version":2 + }, + "email":"test@example.com", + "pickle_timestamp":1572393916428 +} diff --git a/mobile/android/android-components/components/support/migration/src/test/resources/fxa/corrupt-married-v4.json b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/corrupt-married-v4.json new file mode 100644 index 000000000000..d0a916e2f279 --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/corrupt-married-v4.json @@ -0,0 +1,20 @@ +{ + "account_type":"org.mozilla.firefox_fxaccount", + "authoritiesToSyncAutomaticallyMap":{ + "org.mozilla.firefox.db.browser":true + }, + "account_version":3, + "profileServerURI":"https:\/\/profile.accounts.firefox.com\/v1", + "idpServerURI":"https:\/\/api.accounts.firefox.com\/v1", + "tokenServerURI":"https:\/\/token.services.mozilla.com\/1.0\/sync\/1.5", + "profile":"default", + "pickle_version":3, + "bundle":{ + "profile":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"displayName\":\"grisha\",\"amrValues\":[\"pwd\",\"email\"],\"twoFactorAuthentication\":false,\"avatar\":\"https:\\\/\\\/firefoxusercontent.com\\\/76a0f7cac96ecd290fsdf432f23fc86\",\"locale\":\"en-US,en-CA;q=0.7,en;q=0.3\",\"email\":\"test@example.com\",\"avatarDefault\":false}", + "stateLabel":"Married", + "state":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"verified\":true,\"sessionToken\":\"fjsdkfksf3e8f32f23f832fwf32jf89o327u2843gj23\",\"certificate\":\"xNiJ9.exxxxxxxta2V5Ijp7InAiOiJmZTQxMjVlNmNiZmU2ZGQ1YTQxMGE4YjkxNjI5NDUxZjc4ZmY5NmVlN2Y0MDRkYjVkNDhlYmZjNDUzZTI2NTUxOGMzYjExN2VhODg1MWVjMGQxYjhmMjA0MjQ2NmY4MGJlNGFlN2RhZWZlMDc2OTQ5NTEwZDdmNDgzMjUwYjliNTljNjY4MjdkNzA2ODhmZmFlMDZlMGFkOWQxZGI5ODJmMmE5MWNhMDMxNTJmMzMzNDgwYWVhNTg5NGMyNWExMzI3NTNkNjJjZGU4YzkyZTczODZhOTM5ZWYzYTVjZmNhZjc5NDJjOTJjZTE4ZTY3MDFhN2E5MmZjMTk3MTk1MDMxIiwicSI6ImFjYzA3NGQ1MDJiMmRlNmI3YmU5Y2ZmNGY3N2I2Y2EyNTEzMzY1MWQiLCJnIjoiM2NhZWVjMzVmZDlkOGYwZWQ0NjhhYzE4MWE4YWMxYjFhZWQxYmFlNmVkNWJjYTM5ZWNjYTNmMzU5ZGQ3MWZlZjA1OWY1OTEyOWRhMDJlYzAzMTc0MDliMmM2YTRmMGNlYTFlODNhYTljZGI1NGEzOTU2NjVmZmFkOWMzMjlmMGI1NTY0MTI5NjhjNGQxMzIwNzY2NjFjMmQ1ZmM2N2Q5NjE2ZGUxY2EyMmEyMTRhYWM3ZGJkYmE2ZjBjMjNkMzA5NDI3ZjM1NmFjMGNjMTYwOGZiZDc3MjI2N2VmMDYwYzRhZDBmMjg5YWEzYTE0NzBhYmZmYTA0NWZhNzg0N2JmNiIsInkiOiJhMDZlODkzMjk0ZmRjZDVjYWE3YjdlNWIzODFkNmFiYTc4M2Y1M2NmNjlkNjY3YjdkNjRkODY4MDhiZTExZmU1ODU2OWJjYmJlOWYxZmIxMDg3ODMyZjIxZGIwZjMxN2QyMDczYzE4MDYwNWM1M2IxZmU5NDc0MTQyYWM1ODJlMDNiMDI4MGU1YmM4MGI4ZjlmNDY4ZDdlMmExMzE4M2UwZTlmNzllMDY0NDM4OGNjYjYyOWQ4NDBkOGNiNGYxN2EzZTM0OTMxNDE2ZmUzYWM3ZmMwNzNmNGEwMDNhYjY4MDg4OTcxNDc0Njc0ZGQyMzIwODg0NTdmOGRjOTI1MDdlIiwiYWxnb3JpdGhtIjoiRFMifSwicHJpbmNpcGFsIjp7ImVtYWlsIjoiNmFmZTI3NzYzNTJmNDY3N2EwNDk3ODYzN2U1MmI1ZWJAYXBpLmFjY291bnRzLmZpcmVmb3guY29tIn0sImlhdCI6MTU3MDc1MTk4MTA5NiwiZXhwIjoxNTcwNzk1MTkxMDk2LCJmeGEtZ2VuZXJhdGlvbiI6MTU2MzkyNjgwNDkxOCwiZnhhLWxhc3RBdXRoQXQiOjE1NzA3NTE5ODUsImZ4YS12ZXJpZmllZEVtYWlsIjoiZ2tydWdsb3ZAbW96aWxsYS5jb20iLCJmeGEtZGV2aWNlSWQiOiI1YWZhOWUzNmRjY2UyMGUxNTkyNzllZjI4MjI4NjRlNCIsImZ4YS10b2tlblZlcmlmaWVkIjp0cnVlLCJmeGEtYW1yIjpbInB3ZCIsImVtYWlsIl0sImZ4YS1hYWwiOjEsImZ4YS1wcm9maWxlQ2hhbmdlZEF0IjoxNTY4ODMwODIwNzA5LCJpc3MiOiJhcGkuYWNjb3VudHMuZmlyZWZveC5jb20ifQ.jIU9rFBfwynLB-m7bnJTBPdHKoZ-t9keQzWzbdOcpvZN7g3SNpubCK-yIoiq9y_M8xj10_iFVRpm2VCtQu91MAMKPXdp7Wflq_SEHUSuzFsoVfwgueoFjv6g2vESJYK-ri77FUeB01374nn16P6ihgNVsJ9uo6sjgdxZfQuUrpztpv5svADccwmsn0OHqGsb9b2iD74fPgihwljH17lCV-vW4iNrt6KgPoAVlO1wxZcZfr5oJRVJJSE9lr91ksUrAkwEw2_FSFsdfsdf3wwefwefhusfFSDFsdfy732wfmwuFSDfjyDGOae3Jzheatsmg4ZY07NgxQSA\",\"keyPair\":{\"privateKey\":{\"p\":\"fe4125e6cbfe6dd5a410a8b91629451f78ff96ee7f404db5d48ebfc453e265518c3b117ea8851ec0d1b8f2042466f80be4ae7daefFSDFEWfsdfsdff32oufj3266827d70688ffae06e0ad9d1db982f2a91ca03152f333480aea5894c25a132753d62cde8c92e7386a939ef3a5cfcaf7942c92ce18e6701a7a92fc197195031\",\"q\":\"acc074d502b2de6b7be9cff4f77b6ca25133651d\",\"g\":\"3caeec35fd9d8f0ed468ac181a8ac1b1aed1bae6ed5bca39ecca3f359dd71fef059f59129da02ec0317409b2c6a4f0cea1e83aa9cdb54a395665ffad9c329f0b556412968c4d132076661c2d5fc67d9616de1ca22a214aac7dbdba6f0c23d309427f356ac0cc1608fbd772267ef060c4ad0f289aa3a1470abffa045fa7847bf6\",\"x\":\"5bf6bc179e0aa092b1c9f5816881d5b1a20f084b\",\"algorithm\":\"DS\"},\"publicKey\":{\"p\":\"fe4125e6cbfe6dd5a410a8b91629451f78ff96ee7f404db5d48ebfc453e265518c3b117ea8851ec0d1b8f2042466f80be4ae7daefe076949510d7f483250b9b59c66827d70688ffae06e0ad9d1db982f2a91ca03152f333480aea5894c25a132753d62cde8c92e7386a939ef3a5cfcaf7942c92ce18e6701a7a92fc197195031\",\"q\":\"acc074d502b2de6b7be9cff4f77b6ca25133651d\",\"g\":\"3caeec35fd9d8f0ed468ac181a8ac1b1aed1bae6ed5bca39ecca3f359dd71fef059f59129da02ec0317409b2c6a4f0cea1e83aa9cdb54a395665ffad9c329f0b556412968c4d132076661c2d5fc67d9616de1ca22a214aac7dbdba6f0c23d309427f356ac0cc1608fbd772267ef060c4ad0f289aa3a1470abffa045fa7847bf6\",\"y\":\"a06e893294fdcd5caa7b7e5b381d6aba783f53cf69d667b7d64d86808be11fe58569bcbbe9f1fb1087832f21db0f317d2073c180605c53b1fe9474142ac582e03b0280e5bc80b8f9f468d7e2a13183e0e9f79e0644388ccb629d840d8cb4f17a3e34931416fe3ac7fc073f4a003ab68088971474674dd232088457f8dc92507e\",\"algorithm\":\"DS\"}},\"kXCS\":\"0b3ba79bfxdf32f3of32jowef7987f\",\"version\":4,\"kSync\":\"252fsvj8932vj32movj97325hjfksdhfjstrg23yurt267r23\",\"email\":\"test@example.com\"}", + "version":2 + }, + "email":"test@example.com" + "pickle_timestamp":1570752059608 +} diff --git a/mobile/android/android-components/components/support/migration/src/test/resources/fxa/corrupt-separated-missing-versions-v4.json b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/corrupt-separated-missing-versions-v4.json new file mode 100644 index 000000000000..7fb3a4cb87f0 --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/corrupt-separated-missing-versions-v4.json @@ -0,0 +1,19 @@ +{ + "account_type":"org.mozilla.firefox_fxaccount", + "authoritiesToSyncAutomaticallyMap":{ + "org.mozilla.firefox.db.browser":true + }, + "account_version":3, + "profileServerURI":"https:\/\/profile.accounts.firefox.com\/v1", + "idpServerURI":"https:\/\/api.accounts.firefox.com\/v1", + "tokenServerURI":"https:\/\/token.services.mozilla.com\/1.0\/sync\/1.5", + "profile":"default", + "bundle":{ + "profile":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"displayName\":\"grisha\",\"amrValues\":[\"pwd\",\"email\"],\"twoFactorAuthentication\":false,\"avatar\":\"https:\\\/\\\/firefoxusercontent.com\\\/76a0f7cfsdf3d290fef333609f1cc86\",\"locale\":\"en-US,en-CA;q=0.7,en;q=0.3\",\"email\":\"test@example.com\",\"avatarDefault\":false}", + "stateLabel":"Separated", + "state":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"verified\":true,\"version\":4,\"email\":\"test@example.com\"}", + "version":2 + }, + "email":"test@example.com", + "pickle_timestamp":1572394630169 +} diff --git a/mobile/android/android-components/components/support/migration/src/test/resources/fxa/doghouse-v4.json b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/doghouse-v4.json new file mode 100644 index 000000000000..f0ba6ca84e20 --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/doghouse-v4.json @@ -0,0 +1,20 @@ +{ + "account_type":"org.mozilla.firefox_fxaccount", + "authoritiesToSyncAutomaticallyMap":{ + "org.mozilla.firefox.db.browser":true + }, + "account_version":3, + "profileServerURI":"https:\/\/profile.accounts.firefox.com\/v1", + "idpServerURI":"https:\/\/api.accounts.firefox.com\/v1", + "tokenServerURI":"https:\/\/token.services.mozilla.com\/1.0\/sync\/1.5", + "profile":"default", + "pickle_version":3, + "bundle":{ + "profile":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"displayName\":\"grisha\",\"amrValues\":[\"pwd\",\"email\"],\"twoFactorAuthentication\":false,\"avatar\":\"https:\\\/\\\/firefoxusercontent.com\\\/76a0f7cac96ecdfsdf43609f1cc86\",\"locale\":\"en-US,en-CA;q=0.7,en;q=0.3\",\"email\":\"test@example.com\",\"avatarDefault\":false}", + "stateLabel":"Doghouse", + "state":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"verified\":true,\"version\":4,\"email\":\"test@example.com\"}", + "version":2 + }, + "email":"test@example.com", + "pickle_timestamp":1572396171578 +} \ No newline at end of file diff --git a/mobile/android/android-components/components/support/migration/src/test/resources/fxa/married-v4.json b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/married-v4.json new file mode 100644 index 000000000000..07a63f1201a0 --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/married-v4.json @@ -0,0 +1,20 @@ +{ + "account_type":"org.mozilla.firefox_fxaccount", + "authoritiesToSyncAutomaticallyMap":{ + "org.mozilla.firefox.db.browser":true + }, + "account_version":3, + "profileServerURI":"https:\/\/profile.accounts.firefox.com\/v1", + "idpServerURI":"https:\/\/api.accounts.firefox.com\/v1", + "tokenServerURI":"https:\/\/token.services.mozilla.com\/1.0\/sync\/1.5", + "profile":"default", + "pickle_version":3, + "bundle":{ + "profile":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"displayName\":\"grisha\",\"amrValues\":[\"pwd\",\"email\"],\"twoFactorAuthentication\":false,\"avatar\":\"https:\\\/\\\/firefoxusercontent.com\\\/76a0f7cac96ecd290fsdf432f23fc86\",\"locale\":\"en-US,en-CA;q=0.7,en;q=0.3\",\"email\":\"test@example.com\",\"avatarDefault\":false}", + "stateLabel":"Married", + "state":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"verified\":true,\"sessionToken\":\"fjsdkfksf3e8f32f23f832fwf32jf89o327u2843gj23\",\"certificate\":\"xNiJ9.exxxxxxxta2V5Ijp7InAiOiJmZTQxMjVlNmNiZmU2ZGQ1YTQxMGE4YjkxNjI5NDUxZjc4ZmY5NmVlN2Y0MDRkYjVkNDhlYmZjNDUzZTI2NTUxOGMzYjExN2VhODg1MWVjMGQxYjhmMjA0MjQ2NmY4MGJlNGFlN2RhZWZlMDc2OTQ5NTEwZDdmNDgzMjUwYjliNTljNjY4MjdkNzA2ODhmZmFlMDZlMGFkOWQxZGI5ODJmMmE5MWNhMDMxNTJmMzMzNDgwYWVhNTg5NGMyNWExMzI3NTNkNjJjZGU4YzkyZTczODZhOTM5ZWYzYTVjZmNhZjc5NDJjOTJjZTE4ZTY3MDFhN2E5MmZjMTk3MTk1MDMxIiwicSI6ImFjYzA3NGQ1MDJiMmRlNmI3YmU5Y2ZmNGY3N2I2Y2EyNTEzMzY1MWQiLCJnIjoiM2NhZWVjMzVmZDlkOGYwZWQ0NjhhYzE4MWE4YWMxYjFhZWQxYmFlNmVkNWJjYTM5ZWNjYTNmMzU5ZGQ3MWZlZjA1OWY1OTEyOWRhMDJlYzAzMTc0MDliMmM2YTRmMGNlYTFlODNhYTljZGI1NGEzOTU2NjVmZmFkOWMzMjlmMGI1NTY0MTI5NjhjNGQxMzIwNzY2NjFjMmQ1ZmM2N2Q5NjE2ZGUxY2EyMmEyMTRhYWM3ZGJkYmE2ZjBjMjNkMzA5NDI3ZjM1NmFjMGNjMTYwOGZiZDc3MjI2N2VmMDYwYzRhZDBmMjg5YWEzYTE0NzBhYmZmYTA0NWZhNzg0N2JmNiIsInkiOiJhMDZlODkzMjk0ZmRjZDVjYWE3YjdlNWIzODFkNmFiYTc4M2Y1M2NmNjlkNjY3YjdkNjRkODY4MDhiZTExZmU1ODU2OWJjYmJlOWYxZmIxMDg3ODMyZjIxZGIwZjMxN2QyMDczYzE4MDYwNWM1M2IxZmU5NDc0MTQyYWM1ODJlMDNiMDI4MGU1YmM4MGI4ZjlmNDY4ZDdlMmExMzE4M2UwZTlmNzllMDY0NDM4OGNjYjYyOWQ4NDBkOGNiNGYxN2EzZTM0OTMxNDE2ZmUzYWM3ZmMwNzNmNGEwMDNhYjY4MDg4OTcxNDc0Njc0ZGQyMzIwODg0NTdmOGRjOTI1MDdlIiwiYWxnb3JpdGhtIjoiRFMifSwicHJpbmNpcGFsIjp7ImVtYWlsIjoiNmFmZTI3NzYzNTJmNDY3N2EwNDk3ODYzN2U1MmI1ZWJAYXBpLmFjY291bnRzLmZpcmVmb3guY29tIn0sImlhdCI6MTU3MDc1MTk4MTA5NiwiZXhwIjoxNTcwNzk1MTkxMDk2LCJmeGEtZ2VuZXJhdGlvbiI6MTU2MzkyNjgwNDkxOCwiZnhhLWxhc3RBdXRoQXQiOjE1NzA3NTE5ODUsImZ4YS12ZXJpZmllZEVtYWlsIjoiZ2tydWdsb3ZAbW96aWxsYS5jb20iLCJmeGEtZGV2aWNlSWQiOiI1YWZhOWUzNmRjY2UyMGUxNTkyNzllZjI4MjI4NjRlNCIsImZ4YS10b2tlblZlcmlmaWVkIjp0cnVlLCJmeGEtYW1yIjpbInB3ZCIsImVtYWlsIl0sImZ4YS1hYWwiOjEsImZ4YS1wcm9maWxlQ2hhbmdlZEF0IjoxNTY4ODMwODIwNzA5LCJpc3MiOiJhcGkuYWNjb3VudHMuZmlyZWZveC5jb20ifQ.jIU9rFBfwynLB-m7bnJTBPdHKoZ-t9keQzWzbdOcpvZN7g3SNpubCK-yIoiq9y_M8xj10_iFVRpm2VCtQu91MAMKPXdp7Wflq_SEHUSuzFsoVfwgueoFjv6g2vESJYK-ri77FUeB01374nn16P6ihgNVsJ9uo6sjgdxZfQuUrpztpv5svADccwmsn0OHqGsb9b2iD74fPgihwljH17lCV-vW4iNrt6KgPoAVlO1wxZcZfr5oJRVJJSE9lr91ksUrAkwEw2_FSFsdfsdf3wwefwefhusfFSDFsdfy732wfmwuFSDfjyDGOae3Jzheatsmg4ZY07NgxQSA\",\"keyPair\":{\"privateKey\":{\"p\":\"fe4125e6cbfe6dd5a410a8b91629451f78ff96ee7f404db5d48ebfc453e265518c3b117ea8851ec0d1b8f2042466f80be4ae7daefFSDFEWfsdfsdff32oufj3266827d70688ffae06e0ad9d1db982f2a91ca03152f333480aea5894c25a132753d62cde8c92e7386a939ef3a5cfcaf7942c92ce18e6701a7a92fc197195031\",\"q\":\"acc074d502b2de6b7be9cff4f77b6ca25133651d\",\"g\":\"3caeec35fd9d8f0ed468ac181a8ac1b1aed1bae6ed5bca39ecca3f359dd71fef059f59129da02ec0317409b2c6a4f0cea1e83aa9cdb54a395665ffad9c329f0b556412968c4d132076661c2d5fc67d9616de1ca22a214aac7dbdba6f0c23d309427f356ac0cc1608fbd772267ef060c4ad0f289aa3a1470abffa045fa7847bf6\",\"x\":\"5bf6bc179e0aa092b1c9f5816881d5b1a20f084b\",\"algorithm\":\"DS\"},\"publicKey\":{\"p\":\"fe4125e6cbfe6dd5a410a8b91629451f78ff96ee7f404db5d48ebfc453e265518c3b117ea8851ec0d1b8f2042466f80be4ae7daefe076949510d7f483250b9b59c66827d70688ffae06e0ad9d1db982f2a91ca03152f333480aea5894c25a132753d62cde8c92e7386a939ef3a5cfcaf7942c92ce18e6701a7a92fc197195031\",\"q\":\"acc074d502b2de6b7be9cff4f77b6ca25133651d\",\"g\":\"3caeec35fd9d8f0ed468ac181a8ac1b1aed1bae6ed5bca39ecca3f359dd71fef059f59129da02ec0317409b2c6a4f0cea1e83aa9cdb54a395665ffad9c329f0b556412968c4d132076661c2d5fc67d9616de1ca22a214aac7dbdba6f0c23d309427f356ac0cc1608fbd772267ef060c4ad0f289aa3a1470abffa045fa7847bf6\",\"y\":\"a06e893294fdcd5caa7b7e5b381d6aba783f53cf69d667b7d64d86808be11fe58569bcbbe9f1fb1087832f21db0f317d2073c180605c53b1fe9474142ac582e03b0280e5bc80b8f9f468d7e2a13183e0e9f79e0644388ccb629d840d8cb4f17a3e34931416fe3ac7fc073f4a003ab68088971474674dd232088457f8dc92507e\",\"algorithm\":\"DS\"}},\"kXCS\":\"0b3ba79bfxdf32f3of32jowef7987f\",\"version\":4,\"kSync\":\"252fsvj8932vj32movj97325hjfksdhfjstrg23yurt267r23\",\"email\":\"test@example.com\"}", + "version":2 + }, + "email":"test@example.com", + "pickle_timestamp":1570752059608 +} diff --git a/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-account-version-v4.json b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-account-version-v4.json new file mode 100644 index 000000000000..54e98dba808d --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-account-version-v4.json @@ -0,0 +1,20 @@ +{ + "account_type":"org.mozilla.firefox_fxaccount", + "authoritiesToSyncAutomaticallyMap":{ + "org.mozilla.firefox.db.browser":true + }, + "account_version":23, + "profileServerURI":"https:\/\/profile.accounts.firefox.com\/v1", + "idpServerURI":"https:\/\/api.accounts.firefox.com\/v1", + "tokenServerURI":"https:\/\/token.services.mozilla.com\/1.0\/sync\/1.5", + "profile":"default", + "pickle_version":3, + "bundle":{ + "profile":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"displayName\":\"grisha\",\"amrValues\":[\"pwd\",\"email\"],\"twoFactorAuthentication\":false,\"avatar\":\"https:\\\/\\\/firefoxusercontent.com\\\/76a0f7cfsdf3d290fef333609f1cc86\",\"locale\":\"en-US,en-CA;q=0.7,en;q=0.3\",\"email\":\"test@example.com\",\"avatarDefault\":false}", + "stateLabel":"Separated", + "state":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"verified\":true,\"version\":4,\"email\":\"test@example.com\"}", + "version":2 + }, + "email":"test@example.com", + "pickle_timestamp":1572394630169 +} diff --git a/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-pickle-version-v4.json b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-pickle-version-v4.json new file mode 100644 index 000000000000..a4c2cc08253d --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-pickle-version-v4.json @@ -0,0 +1,20 @@ +{ + "account_type":"org.mozilla.firefox_fxaccount", + "authoritiesToSyncAutomaticallyMap":{ + "org.mozilla.firefox.db.browser":true + }, + "account_version":3, + "profileServerURI":"https:\/\/profile.accounts.firefox.com\/v1", + "idpServerURI":"https:\/\/api.accounts.firefox.com\/v1", + "tokenServerURI":"https:\/\/token.services.mozilla.com\/1.0\/sync\/1.5", + "profile":"default", + "pickle_version":34, + "bundle":{ + "profile":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"displayName\":\"grisha\",\"amrValues\":[\"pwd\",\"email\"],\"twoFactorAuthentication\":false,\"avatar\":\"https:\\\/\\\/firefoxusercontent.com\\\/76a0f7cfsdf3d290fef333609f1cc86\",\"locale\":\"en-US,en-CA;q=0.7,en;q=0.3\",\"email\":\"test@example.com\",\"avatarDefault\":false}", + "stateLabel":"Separated", + "state":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"verified\":true,\"version\":4,\"email\":\"test@example.com\"}", + "version":2 + }, + "email":"test@example.com", + "pickle_timestamp":1572394630169 +} diff --git a/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-state-version-v10.json b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-state-version-v10.json new file mode 100644 index 000000000000..98d9419b9802 --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-state-version-v10.json @@ -0,0 +1,20 @@ +{ + "account_type":"org.mozilla.firefox_fxaccount", + "authoritiesToSyncAutomaticallyMap":{ + "org.mozilla.firefox.db.browser":true + }, + "account_version":3, + "profileServerURI":"https:\/\/profile.accounts.firefox.com\/v1", + "idpServerURI":"https:\/\/api.accounts.firefox.com\/v1", + "tokenServerURI":"https:\/\/token.services.mozilla.com\/1.0\/sync\/1.5", + "profile":"default", + "pickle_version":3, + "bundle":{ + "profile":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"displayName\":\"grisha\",\"amrValues\":[\"pwd\",\"email\"],\"twoFactorAuthentication\":false,\"avatar\":\"https:\\\/\\\/firefoxusercontent.com\\\/76a0f7cfsdf3d290fef333609f1cc86\",\"locale\":\"en-US,en-CA;q=0.7,en;q=0.3\",\"email\":\"test@example.com\",\"avatarDefault\":false}", + "stateLabel":"Separated", + "state":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"verified\":true,\"version\":10,\"email\":\"test@example.com\"}", + "version":2 + }, + "email":"test@example.com", + "pickle_timestamp":1572394630169 +} diff --git a/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-state.json b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-state.json new file mode 100644 index 000000000000..be358586c959 --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-bad-state.json @@ -0,0 +1,20 @@ +{ + "account_type":"org.mozilla.firefox_fxaccount", + "authoritiesToSyncAutomaticallyMap":{ + "org.mozilla.firefox.db.browser":true + }, + "account_version":3, + "profileServerURI":"https:\/\/profile.accounts.firefox.com\/v1", + "idpServerURI":"https:\/\/api.accounts.firefox.com\/v1", + "tokenServerURI":"https:\/\/token.services.mozilla.com\/1.0\/sync\/1.5", + "profile":"default", + "pickle_version":3, + "bundle":{ + "profile":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"displayName\":\"grisha\",\"amrValues\":[\"pwd\",\"email\"],\"twoFactorAuthentication\":false,\"avatar\":\"https:\\\/\\\/firefoxusercontent.com\\\/76a0f7cfsdf3d290fef333609f1cc86\",\"locale\":\"en-US,en-CA;q=0.7,en;q=0.3\",\"email\":\"test@example.com\",\"avatarDefault\":false}", + "stateLabel":"Separated", + "state":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"verified\":true}", + "version":2 + }, + "email":"test@example.com", + "pickle_timestamp":1572394630169 +} diff --git a/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-v4.json b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-v4.json new file mode 100644 index 000000000000..814f81dd30fc --- /dev/null +++ b/mobile/android/android-components/components/support/migration/src/test/resources/fxa/separated-v4.json @@ -0,0 +1,20 @@ +{ + "account_type":"org.mozilla.firefox_fxaccount", + "authoritiesToSyncAutomaticallyMap":{ + "org.mozilla.firefox.db.browser":true + }, + "account_version":3, + "profileServerURI":"https:\/\/profile.accounts.firefox.com\/v1", + "idpServerURI":"https:\/\/api.accounts.firefox.com\/v1", + "tokenServerURI":"https:\/\/token.services.mozilla.com\/1.0\/sync\/1.5", + "profile":"default", + "pickle_version":3, + "bundle":{ + "profile":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"displayName\":\"grisha\",\"amrValues\":[\"pwd\",\"email\"],\"twoFactorAuthentication\":false,\"avatar\":\"https:\\\/\\\/firefoxusercontent.com\\\/76a0f7cfsdf3d290fef333609f1cc86\",\"locale\":\"en-US,en-CA;q=0.7,en;q=0.3\",\"email\":\"test@example.com\",\"avatarDefault\":false}", + "stateLabel":"Separated", + "state":"{\"uid\":\"6afe2776352f4677a04978637e52b5eb\",\"verified\":true,\"version\":4,\"email\":\"test@example.com\"}", + "version":2 + }, + "email":"test@example.com", + "pickle_timestamp":1572394630169 +} -- 2.11.4.GIT