summaryrefslogtreecommitdiffstats
path: root/mobile/android/android-components/components/service/firefox-accounts
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
commit8dd16259287f58f9273002717ec4d27e97127719 (patch)
tree3863e62a53829a84037444beab3abd4ed9dfc7d0 /mobile/android/android-components/components/service/firefox-accounts
parentReleasing progress-linux version 126.0.1-1~progress7.99u1. (diff)
downloadfirefox-8dd16259287f58f9273002717ec4d27e97127719.tar.xz
firefox-8dd16259287f58f9273002717ec4d27e97127719.zip
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/android-components/components/service/firefox-accounts')
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt19
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt11
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt3
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt25
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt84
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt31
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt6
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt5
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt1
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt32
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt192
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt4
-rw-r--r--mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt106
13 files changed, 358 insertions, 161 deletions
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt
index 37af0f5b76..e5f8a665eb 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/AccountStorage.kt
@@ -11,7 +11,6 @@ import mozilla.appservices.fxaclient.FxaRustAuthState
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.concept.sync.AccountEvent
import mozilla.components.concept.sync.AccountEventsObserver
-import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.StatePersistenceCallback
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
import mozilla.components.service.fxa.manager.FxaAccountManager
@@ -26,16 +25,16 @@ const val FXA_STATE_KEY = "fxaState"
* Represents state of our account on disk - is it new, or restored?
*/
internal sealed class AccountOnDisk : WithAccount {
- data class Restored(val account: OAuthAccount) : AccountOnDisk() {
+ data class Restored(val account: FirefoxAccount) : AccountOnDisk() {
override fun account() = account
}
- data class New(val account: OAuthAccount) : AccountOnDisk() {
+ data class New(val account: FirefoxAccount) : AccountOnDisk() {
override fun account() = account
}
}
internal interface WithAccount {
- fun account(): OAuthAccount
+ fun account(): FirefoxAccount
}
/**
@@ -80,16 +79,16 @@ open class StorageWrapper(
}
}
- private fun watchAccount(account: OAuthAccount) {
+ private fun watchAccount(account: FirefoxAccount) {
account.registerPersistenceCallback(statePersistenceCallback)
account.deviceConstellation().register(accountEventsIntegration)
}
/**
- * Exists strictly for testing purposes, allowing tests to specify their own implementation of [OAuthAccount].
+ * Exists strictly for testing purposes, allowing tests to specify their own implementation of [FirefoxAccount].
*/
@VisibleForTesting
- open fun obtainAccount(): OAuthAccount = FirefoxAccount(serverConfig, crashReporter)
+ open fun obtainAccount(): FirefoxAccount = FirefoxAccount(serverConfig, crashReporter)
}
/**
@@ -110,7 +109,7 @@ internal class AccountEventsIntegration(
internal interface AccountStorage {
@Throws(Exception::class)
- fun read(): OAuthAccount?
+ fun read(): FirefoxAccount?
fun write(accountState: String)
fun clear()
}
@@ -156,7 +155,7 @@ internal class SharedPrefAccountStorage(
* @throws FxaException if JSON failed to parse into a [FirefoxAccount].
*/
@Throws(FxaException::class)
- override fun read(): OAuthAccount? {
+ override fun read(): FirefoxAccount? {
val savedJSON = accountPreferences().getString(FXA_STATE_KEY, null)
?: return null
@@ -244,7 +243,7 @@ internal class SecureAbove22AccountStorage(
* @throws FxaException if JSON failed to parse into a [FirefoxAccount].
*/
@Throws(FxaException::class)
- override fun read(): OAuthAccount? {
+ override fun read(): FirefoxAccount? {
return store.getString(KEY_ACCOUNT_STATE).also {
// If account state is missing, but we expected it to be present, report an exception.
if (it == null && prefs.getBoolean(PREF_KEY_HAS_STATE, false)) {
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
index 7fc31785e3..14e3e7d105 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FirefoxAccount.kt
@@ -17,6 +17,7 @@ import mozilla.components.concept.sync.DeviceConstellation
import mozilla.components.concept.sync.FxAEntryPoint
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.StatePersistenceCallback
+import mozilla.components.concept.sync.UserData
import mozilla.components.support.base.log.logger.Logger
typealias PersistCallback = mozilla.appservices.fxaclient.FxaClient.PersistCallback
@@ -96,6 +97,10 @@ class FirefoxAccount internal constructor(
internal fun getAuthState() = inner.getAuthState()
+ internal fun simulateNetworkError() = inner.simulateNetworkError()
+ internal fun simulateTemporaryAuthTokenIssue() = inner.simulateTemporaryAuthTokenIssue()
+ internal fun simulatePermanentAuthTokenIssue() = inner.simulatePermanentAuthTokenIssue()
+
override suspend fun beginOAuthFlow(
scopes: Set<String>,
entryPoint: FxAEntryPoint,
@@ -128,6 +133,12 @@ class FirefoxAccount internal constructor(
}
}
+ override suspend fun setUserData(userData: UserData) {
+ handleFxaExceptions(logger, "setUserData", { null }) {
+ inner.setUserData(userData.into())
+ }
+ }
+
override fun getCurrentDeviceId(): String? {
// This is awkward, yes. Underlying method simply reads some data from in-memory state, and yet it throws
// in case that data isn't there. See https://github.com/mozilla/application-services/issues/2202.
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt
index 99c4fb196f..88e4eebd4c 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/FxaDeviceConstellation.kt
@@ -191,6 +191,9 @@ class FxaDeviceConstellation(
crashReporter?.submitCaughtException(error)
}
}
+ is DeviceCommandOutgoing.CloseTab -> {
+ account.closeTabs(targetDeviceId, outgoingCommand.urls)
+ }
else -> logger.debug("Skipped sending unsupported command type: $outgoingCommand")
}
null
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
index 737ff3c273..d489686cdd 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/Types.kt
@@ -18,6 +18,7 @@ import mozilla.components.concept.sync.DeviceCapability
import mozilla.components.concept.sync.DeviceType
import mozilla.components.concept.sync.OAuthScopedKey
import mozilla.components.concept.sync.SyncAuthInfo
+import mozilla.components.concept.sync.UserData
import mozilla.appservices.fxaclient.DeviceCapability as RustDeviceCapability
import mozilla.appservices.fxaclient.DevicePushSubscription as RustDevicePushSubscription
import mozilla.appservices.sync15.DeviceType as RustDeviceType
@@ -106,6 +107,20 @@ fun Profile.into(): mozilla.components.concept.sync.Profile {
)
}
+/**
+ * Converts the android-components defined [UserData] type into
+ * the application-services one, so consumers of android-components
+ * do not have to know about application services.
+ */
+fun UserData.into(): mozilla.appservices.fxaclient.UserData {
+ return mozilla.appservices.fxaclient.UserData(
+ sessionToken,
+ uid,
+ email,
+ verified,
+ )
+}
+
internal fun RustDeviceType.into(): DeviceType {
return when (this) {
RustDeviceType.DESKTOP -> DeviceType.DESKTOP
@@ -139,6 +154,7 @@ fun DeviceType.into(): RustDeviceType {
fun DeviceCapability.into(): RustDeviceCapability {
return when (this) {
DeviceCapability.SEND_TAB -> RustDeviceCapability.SEND_TAB
+ DeviceCapability.CLOSE_TABS -> RustDeviceCapability.CLOSE_TABS
}
}
@@ -149,6 +165,7 @@ fun DeviceCapability.into(): RustDeviceCapability {
fun RustDeviceCapability.into(): DeviceCapability {
return when (this) {
RustDeviceCapability.SEND_TAB -> DeviceCapability.SEND_TAB
+ RustDeviceCapability.CLOSE_TABS -> DeviceCapability.CLOSE_TABS
}
}
@@ -240,6 +257,7 @@ fun AccountEvent.into(): mozilla.components.concept.sync.AccountEvent {
fun IncomingDeviceCommand.into(): mozilla.components.concept.sync.DeviceCommandIncoming {
return when (this) {
is IncomingDeviceCommand.TabReceived -> this.into()
+ is IncomingDeviceCommand.TabsClosed -> this.into()
}
}
@@ -249,3 +267,10 @@ fun IncomingDeviceCommand.TabReceived.into(): mozilla.components.concept.sync.De
entries = this.payload.entries.map { it.into() },
)
}
+
+fun IncomingDeviceCommand.TabsClosed.into(): mozilla.components.concept.sync.DeviceCommandIncoming.TabsClosed {
+ return mozilla.components.concept.sync.DeviceCommandIncoming.TabsClosed(
+ from = this.sender?.into(),
+ urls = this.payload.urls,
+ )
+}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt
index 03a7d25635..71232c0815 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/FxaAccountManager.kt
@@ -8,9 +8,11 @@ import android.content.Context
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleOwner
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.fxaclient.FxaStateCheckerEvent
import mozilla.appservices.fxaclient.FxaStateCheckerState
@@ -26,6 +28,7 @@ import mozilla.components.concept.sync.FxAEntryPoint
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import mozilla.components.concept.sync.ServiceResult
+import mozilla.components.concept.sync.UserData
import mozilla.components.service.fxa.AccessTokenUnexpectedlyWithoutKey
import mozilla.components.service.fxa.AccountManagerException
import mozilla.components.service.fxa.AccountOnDisk
@@ -344,11 +347,14 @@ open class FxaAccountManager(
* @param pairingUrl Optional pairing URL in case a pairing flow is being initiated.
* @param entrypoint an enum representing the feature entrypoint requesting the URL.
* the entrypoint is used in telemetry.
+ * @param authScopes The oAuth scopes being requested, if none are provided
+ * we default to the scopes provided when constructing [FxaAccountManager]
* @return An authentication url which is to be presented to the user.
*/
suspend fun beginAuthentication(
pairingUrl: String? = null,
entrypoint: FxAEntryPoint,
+ authScopes: Set<String> = scopes,
): String? = withContext(coroutineContext) {
// It's possible that at this point authentication is considered to be "in-progress".
// For example, if user started authentication flow, but cancelled it (closing a custom tab)
@@ -357,9 +363,9 @@ open class FxaAccountManager(
processQueue(Event.Progress.CancelAuth)
val event = if (pairingUrl != null) {
- Event.Account.BeginPairingFlow(pairingUrl, entrypoint)
+ Event.Account.BeginPairingFlow(pairingUrl, entrypoint, authScopes)
} else {
- Event.Account.BeginEmailFlow(entrypoint)
+ Event.Account.BeginEmailFlow(entrypoint, authScopes)
}
// Process the event, then use the new state to check the result of the operation
@@ -375,6 +381,16 @@ open class FxaAccountManager(
}
/**
+ * Sets the user's data received from the web content.
+ * **NOTE**: This is only useful for applications that are user agents, that
+ * require the user's session token, and thus isn't a part of the state machine
+ * @param userData: The user's data as given by the web channel, including the session token
+ */
+ suspend fun setUserData(userData: UserData) = withContext(coroutineContext) {
+ account.setUserData(userData)
+ }
+
+ /**
* Finalize authentication that was started via [beginAuthentication].
*
* If authentication wasn't started via this manager we won't accept this authentication attempt,
@@ -590,10 +606,10 @@ open class FxaAccountManager(
} else {
null
}
- val entrypoint = if (via is Event.Account.BeginEmailFlow) {
- via.entrypoint
+ val (entrypoint, authScopes) = if (via is Event.Account.BeginEmailFlow) {
+ Pair(via.entrypoint, via.scopes)
} else if (via is Event.Account.BeginPairingFlow) {
- via.entrypoint
+ Pair(via.entrypoint, via.scopes)
} else {
// This should be impossible, both `BeginPairingFlow` and `BeginEmailFlow`
// have a required `entrypoint` and we are matching against only instances
@@ -601,7 +617,7 @@ open class FxaAccountManager(
throw IllegalStateException("BeginningAuthentication with a flow that is neither email nor pairing")
}
val result = withRetries(logger, MAX_NETWORK_RETRIES) {
- pairingUrl.asAuthFlowUrl(account, scopes, entrypoint = entrypoint)
+ pairingUrl.asAuthFlowUrl(account, authScopes, entrypoint = entrypoint)
}
when (result) {
is Result.Success -> {
@@ -935,4 +951,60 @@ open class FxaAccountManager(
accountManager.syncStatusObserverRegistry.notifyObservers { onError(error) }
}
}
+
+ /**
+ * Hook this up to the secret debug menu to simulate a network error
+ *
+ * Typical usage is:
+ * - `adb logcat | grep fxa_client`
+ * - Trigger this via the secret debug menu item.
+ * - Watch the logs. You should see the client perform a call to `get_profile', see a
+ * network error, then recover.
+ * - Note: the logs will be more clear once we switch the code to using the app-services state
+ * machine.
+ * - Check the UI, it should be in an authenticated state.
+ */
+ public fun simulateNetworkError() {
+ account.simulateNetworkError()
+ CoroutineScope(coroutineContext).launch {
+ refreshProfile(true)
+ }
+ }
+
+ /**
+ * Hook this up to the secret debug menu to simulate a temporary auth error
+ *
+ * Typical usage is:
+ * - `adb logcat | grep fxa_client`
+ * - Trigger this via the secret debug menu item.
+ * - Watch the logs. You should see the client perform a call to `get_profile', see an
+ * auth error, then recover.
+ * - Check the UI, it should be in an authenticated state.
+ */
+ public fun simulateTemporaryAuthTokenIssue() {
+ account.simulateTemporaryAuthTokenIssue()
+ SyncAuthInfoCache(context).clear()
+ CoroutineScope(coroutineContext).launch {
+ refreshProfile(true)
+ }
+ }
+
+ /**
+ * Hook this up to the secret debug menu to simulate an unrecoverable auth error
+ *
+ * Typical usage is:
+ * - `adb logcat | grep fxa_client`
+ * - Trigger this via the secret debug menu item.
+ * - Initiaite a sync, or perform some other action that requires authentication.
+ * - Watch the logs. You should see the client perform a call to `get_profile', see an
+ * auth error, then fail to recover.
+ * - Check the UI, it should be in an authentication problems state.
+ */
+ public fun simulatePermanentAuthTokenIssue() {
+ account.simulatePermanentAuthTokenIssue()
+ SyncAuthInfoCache(context).clear()
+ CoroutineScope(coroutineContext).launch {
+ refreshProfile(true)
+ }
+ }
}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt
index 111715703a..50858c2bbe 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/manager/State.kt
@@ -52,10 +52,31 @@ import mozilla.components.service.fxa.FxaAuthData
* State transitions are described by a transition matrix, which is described in [State.next].
*/
-internal sealed class AccountState {
+/**
+ * Represents a [State.Idle] in the accounts state machine detailing the state of the account
+ * lifecycle.
+ */
+sealed class AccountState {
+ /**
+ * Account is logged in and authenticated.
+ */
object Authenticated : AccountState()
+
+ /**
+ * Account is authenticating.
+ *
+ * @property oAuthUrl OAuth URL to be loaded to go through the authentication flow.
+ */
data class Authenticating(val oAuthUrl: String) : AccountState()
+
+ /**
+ * Account needs to be re-authenticated (e.g. due to a password change).
+ */
object AuthenticationProblem : AccountState()
+
+ /**
+ * No authenticated account is available (e.g. account is logged out).
+ */
object NotAuthenticated : AccountState()
}
@@ -70,8 +91,12 @@ internal enum class ProgressState {
internal sealed class Event {
internal sealed class Account : Event() {
internal object Start : Account()
- data class BeginEmailFlow(val entrypoint: FxAEntryPoint) : Account()
- data class BeginPairingFlow(val pairingUrl: String?, val entrypoint: FxAEntryPoint) : Account()
+ data class BeginEmailFlow(val entrypoint: FxAEntryPoint, val scopes: Set<String>) : Account()
+ data class BeginPairingFlow(
+ val pairingUrl: String?,
+ val entrypoint: FxAEntryPoint,
+ val scopes: Set<String>,
+ ) : Account()
data class AuthenticationError(val operation: String, val errorCountWithinTheTimeWindow: Int = 1) : Account() {
override fun toString(): String {
return "${this.javaClass.simpleName} - $operation"
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt
index b09e7f3dcc..eb2d7960cf 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncAction.kt
@@ -6,6 +6,7 @@ package mozilla.components.service.fxa.store
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.lib.state.Action
+import mozilla.components.service.fxa.manager.AccountState
/**
* Actions for updating the global [SyncState] via [SyncStore].
@@ -22,6 +23,11 @@ sealed class SyncAction : Action {
data class UpdateAccount(val account: Account?) : SyncAction()
/**
+ * Update the [SyncState.accountState] of the [SyncStore].
+ */
+ data class UpdateAccountState(val accountState: AccountState) : SyncAction()
+
+ /**
* Update the [SyncState.constellationState] of the [SyncStore].
*/
data class UpdateDeviceConstellation(val deviceConstellation: ConstellationState) : SyncAction()
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt
index 86c9d05d2a..91e3870251 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncState.kt
@@ -9,18 +9,21 @@ import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import mozilla.components.lib.state.State
+import mozilla.components.service.fxa.manager.AccountState
import mozilla.components.service.fxa.sync.WorkManagerSyncManager
/**
* Global state of Sync.
*
* @property status The current status of Sync.
- * @property account The current Sync account, if any.
+ * @property account The current Sync [Account], if any.
+ * @property accountState The current [AccountState] of Sync.
* @property constellationState The current constellation state, if any.
*/
data class SyncState(
val status: SyncStatus = SyncStatus.NotInitialized,
val account: Account? = null,
+ val accountState: AccountState = AccountState.NotAuthenticated,
val constellationState: ConstellationState? = null,
) : State
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt
index 50f4b6747e..487d9bd1ea 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStore.kt
@@ -22,6 +22,7 @@ private fun reduce(syncState: SyncState, syncAction: SyncAction): SyncState {
return when (syncAction) {
is SyncAction.UpdateSyncStatus -> syncState.copy(status = syncAction.status)
is SyncAction.UpdateAccount -> syncState.copy(account = syncAction.account)
+ is SyncAction.UpdateAccountState -> syncState.copy(accountState = syncAction.accountState)
is SyncAction.UpdateDeviceConstellation ->
syncState.copy(constellationState = syncAction.deviceConstellation)
}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt
index 4feda96e60..7674d8a8fc 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/main/java/mozilla/components/service/fxa/store/SyncStoreSupport.kt
@@ -10,12 +10,15 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.AccountObserver
+import mozilla.components.concept.sync.AuthFlowError
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.DeviceConstellationObserver
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
+import mozilla.components.service.fxa.manager.AccountState
import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.SCOPE_PROFILE
import mozilla.components.service.fxa.sync.SyncStatusObserver
import java.lang.Exception
@@ -95,6 +98,24 @@ internal class FxaAccountObserver(
private val autoPause: Boolean,
private val coroutineScope: CoroutineScope,
) : AccountObserver {
+ override fun onReady(authenticatedAccount: OAuthAccount?) {
+ coroutineScope.launch {
+ if (authenticatedAccount == null) {
+ return@launch
+ }
+
+ val syncAccount =
+ authenticatedAccount.getProfile()?.toAccount(authenticatedAccount) ?: return@launch
+ store.dispatch(SyncAction.UpdateAccount(account = syncAccount))
+
+ val accountState = when (authenticatedAccount.checkAuthorizationStatus(SCOPE_PROFILE)) {
+ true -> AccountState.Authenticated
+ null, false -> AccountState.AuthenticationProblem
+ }
+ store.dispatch(SyncAction.UpdateAccountState(accountState = accountState))
+ }
+ }
+
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
coroutineScope.launch(Dispatchers.Main) {
account.deviceConstellation().registerDeviceObserver(
@@ -106,12 +127,23 @@ internal class FxaAccountObserver(
coroutineScope.launch {
val syncAccount = account.getProfile()?.toAccount(account) ?: return@launch
store.dispatch(SyncAction.UpdateAccount(syncAccount))
+ store.dispatch(SyncAction.UpdateAccountState(AccountState.Authenticated))
}
}
+ override fun onAuthenticationProblems() {
+ store.dispatch(SyncAction.UpdateAccountState(accountState = AccountState.AuthenticationProblem))
+ }
+
override fun onLoggedOut() {
store.dispatch(SyncAction.UpdateSyncStatus(SyncStatus.LoggedOut))
store.dispatch(SyncAction.UpdateAccount(null))
+ store.dispatch(SyncAction.UpdateAccountState(accountState = AccountState.NotAuthenticated))
+ }
+
+ override fun onFlowError(error: AuthFlowError) {
+ store.dispatch(SyncAction.UpdateAccount(account = null))
+ store.dispatch(SyncAction.UpdateAccountState(accountState = AccountState.NotAuthenticated))
}
}
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
index afc2fc3ee9..3c6a3421f2 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/FxaAccountManagerTest.kt
@@ -74,13 +74,13 @@ internal class TestableStorageWrapper(
manager: FxaAccountManager,
accountEventObserverRegistry: ObserverRegistry<AccountEventsObserver>,
serverConfig: FxaConfig,
- private val block: () -> OAuthAccount = {
- val account: OAuthAccount = mock()
+ private val block: () -> FirefoxAccount = {
+ val account: FirefoxAccount = mock()
`when`(account.deviceConstellation()).thenReturn(mock())
account
},
) : StorageWrapper(manager, accountEventObserverRegistry, serverConfig) {
- override fun obtainAccount(): OAuthAccount = block()
+ override fun obtainAccount(): FirefoxAccount = block()
}
// Same as the actual account manager, except we get to control how FirefoxAccountShaped instances
@@ -95,8 +95,8 @@ internal open class TestableFxaAccountManager(
syncConfig: SyncConfig? = null,
coroutineContext: CoroutineContext,
crashReporter: CrashReporting? = null,
- block: () -> OAuthAccount = {
- val account: OAuthAccount = mock()
+ block: () -> FirefoxAccount = {
+ val account: FirefoxAccount = mock()
`when`(account.deviceConstellation()).thenReturn(mock())
account
},
@@ -201,7 +201,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
- val account = StatePersistenceTestableAccount(profile, constellation)
+ val account = statePersistenceTestableAccount(profile, constellation)
val manager = TestableFxaAccountManager(
testContext,
@@ -218,17 +218,18 @@ class FxaAccountManagerTest {
// We have an account at the start.
`when`(accountStorage.read()).thenReturn(account)
- assertNull(account.persistenceCallback)
+ verify(account, never()).registerPersistenceCallback(any())
manager.start()
// Assert that persistence callback is set.
- assertNotNull(account.persistenceCallback)
+ val captor = argumentCaptor<StatePersistenceCallback>()
+ verify(account).registerPersistenceCallback(captor.capture())
// Assert that ensureCapabilities fired, but not the device initialization (since we're restoring).
verify(constellation).finalizeDevice(eq(AuthType.Existing), any())
// Assert that persistence callback is interacting with the storage layer.
- account.persistenceCallback!!.persist("test")
+ captor.value.persist("test")
verify(accountStorage).write("test")
}
@@ -237,7 +238,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
- val account = StatePersistenceTestableAccount(profile, constellation)
+ val account = statePersistenceTestableAccount(profile, constellation)
val manager = TestableFxaAccountManager(
testContext,
@@ -254,17 +255,18 @@ class FxaAccountManagerTest {
// We have an account at the start.
`when`(accountStorage.read()).thenReturn(account)
- assertNull(account.persistenceCallback)
+ verify(account, never()).registerPersistenceCallback(any())
manager.start()
// Assert that persistence callback is set.
- assertNotNull(account.persistenceCallback)
+ val captor = argumentCaptor<StatePersistenceCallback>()
+ verify(account).registerPersistenceCallback(captor.capture())
// Assert that finalizeDevice fired with a correct auth type. 3 times since we re-try.
verify(constellation, times(3)).finalizeDevice(eq(AuthType.Existing), any())
// Assert that persistence callback is interacting with the storage layer.
- account.persistenceCallback!!.persist("test")
+ captor.value.persist("test")
verify(accountStorage).write("test")
// Since we weren't able to finalize the account state, we're no longer authenticated.
@@ -276,7 +278,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
- val account = StatePersistenceTestableAccount(profile, constellation, ableToRecoverFromAuthError = false)
+ val account = statePersistenceTestableAccount(profile, constellation, ableToRecoverFromAuthError = false)
val accountObserver: AccountObserver = mock()
val manager = TestableFxaAccountManager(
@@ -295,19 +297,19 @@ class FxaAccountManagerTest {
// We have an account at the start.
`when`(accountStorage.read()).thenReturn(account)
- assertNull(account.persistenceCallback)
+ verify(account, never()).registerPersistenceCallback(any())
assertFalse(manager.accountNeedsReauth())
- assertFalse(account.authErrorDetectedCalled)
- assertFalse(account.checkAuthorizationStatusCalled)
+ verify(account, never()).authErrorDetected()
+ verify(account, never()).checkAuthorizationStatus(any())
verify(accountObserver, never()).onAuthenticationProblems()
manager.start()
assertTrue(manager.accountNeedsReauth())
verify(accountObserver, times(1)).onAuthenticationProblems()
- assertTrue(account.authErrorDetectedCalled)
- assertTrue(account.checkAuthorizationStatusCalled)
+ verify(account).authErrorDetected()
+ verify(account).checkAuthorizationStatus(any())
}
@Test(expected = FxaPanicException::class)
@@ -315,7 +317,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile("testUid", "test@example.com", null, "Test Profile")
val constellation: DeviceConstellation = mock()
- val account = StatePersistenceTestableAccount(profile, constellation)
+ val account = statePersistenceTestableAccount(profile, constellation)
val accountObserver: AccountObserver = mock()
val manager = TestableFxaAccountManager(
@@ -339,7 +341,7 @@ class FxaAccountManagerTest {
// We have an account at the start.
`when`(accountStorage.read()).thenReturn(account)
- assertNull(account.persistenceCallback)
+ verify(account, never()).registerPersistenceCallback(any())
assertFalse(manager.accountNeedsReauth())
verify(accountObserver, never()).onAuthenticationProblems()
@@ -352,7 +354,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
- val account = StatePersistenceTestableAccount(profile, constellation)
+ val account = statePersistenceTestableAccount(profile, constellation)
val accountObserver: AccountObserver = mock()
// We are not using the "prepareHappy..." helper method here, because our account isn't a mock,
// but an actual implementation of the interface.
@@ -388,7 +390,9 @@ class FxaAccountManagerTest {
verify(constellation).finalizeDevice(eq(AuthType.Signin), any())
// Assert that persistence callback is interacting with the storage layer.
- account.persistenceCallback!!.persist("test")
+ val captor = argumentCaptor<StatePersistenceCallback>()
+ verify(account).registerPersistenceCallback(captor.capture())
+ captor.value.persist("test")
verify(accountStorage).write("test")
}
@@ -397,7 +401,7 @@ class FxaAccountManagerTest {
val accountStorage: AccountStorage = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val constellation: DeviceConstellation = mockDeviceConstellation()
- val account = StatePersistenceTestableAccount(profile, constellation)
+ val account = statePersistenceTestableAccount(profile, constellation)
val accountObserver: AccountObserver = mock()
// We are not using the "prepareHappy..." helper method here, because our account isn't a mock,
// but an actual implementation of the interface.
@@ -423,7 +427,7 @@ class FxaAccountManagerTest {
manager.finishAuthentication(FxaAuthData(AuthType.Signin, "dummyCode", UNEXPECTED_AUTH_STATE))
assertTrue(manager.authenticatedAccount() == null)
- // Start authentication. StatePersistenceTestableAccount will produce state=EXPECTED_AUTH_STATE.
+ // Start authentication. statePersistenceTestableAccount will produce state=EXPECTED_AUTH_STATE.
assertEquals(testAuthFlowUrl(entrypoint = "home-menu").url, manager.beginAuthentication(entrypoint = entryPoint))
// Now attempt to finish it with a correct state.
@@ -435,97 +439,17 @@ class FxaAccountManagerTest {
assertEquals(account, manager.authenticatedAccount())
}
- class StatePersistenceTestableAccount(
- private val profile: Profile,
- private val constellation: DeviceConstellation,
- val ableToRecoverFromAuthError: Boolean = false,
- val tokenServerEndpointUrl: String? = null,
- val accessToken: (() -> AccessTokenInfo)? = null,
- ) : OAuthAccount {
-
- var persistenceCallback: StatePersistenceCallback? = null
- var checkAuthorizationStatusCalled = false
- var authErrorDetectedCalled = false
-
- override suspend fun beginOAuthFlow(scopes: Set<String>, entryPoint: FxAEntryPoint): AuthFlowUrl? {
- return AuthFlowUrl(EXPECTED_AUTH_STATE, testAuthFlowUrl(entrypoint = entryPoint.entryName).url)
- }
-
- override suspend fun beginPairingFlow(pairingUrl: String, scopes: Set<String>, entryPoint: FxAEntryPoint): AuthFlowUrl? {
- return AuthFlowUrl(EXPECTED_AUTH_STATE, testAuthFlowUrl(entrypoint = entryPoint.entryName).url)
- }
-
- override suspend fun getProfile(ignoreCache: Boolean): Profile? {
- return profile
- }
-
- override fun getCurrentDeviceId(): String? {
- return "testFxaDeviceId"
- }
-
- override fun getSessionToken(): String? {
- return null
- }
-
- override suspend fun completeOAuthFlow(code: String, state: String): Boolean {
- return true
- }
-
- override suspend fun getAccessToken(singleScope: String): AccessTokenInfo? {
- val token = accessToken?.invoke()
- if (token != null) return token
-
- fail()
- return null
- }
-
- override fun authErrorDetected() {
- authErrorDetectedCalled = true
- }
-
- override suspend fun checkAuthorizationStatus(singleScope: String): Boolean? {
- checkAuthorizationStatusCalled = true
- return ableToRecoverFromAuthError
- }
-
- override suspend fun getTokenServerEndpointURL(): String? {
- if (tokenServerEndpointUrl != null) return tokenServerEndpointUrl
-
- fail()
- return ""
- }
-
- override suspend fun getManageAccountURL(entryPoint: FxAEntryPoint): String? {
- return "https://firefox.com/settings"
- }
-
- override fun getPairingAuthorityURL(): String {
- return "https://firefox.com/pair"
- }
-
- override fun registerPersistenceCallback(callback: StatePersistenceCallback) {
- persistenceCallback = callback
- }
-
- override fun deviceConstellation(): DeviceConstellation {
- return constellation
- }
-
- override suspend fun disconnect(): Boolean {
- return true
- }
-
- override fun toJSONString(): String {
- fail()
- return ""
- }
-
- override fun close() {
- // Only expect 'close' to be called if we can't recover from an auth error.
- if (ableToRecoverFromAuthError) {
- fail()
- }
- }
+ suspend fun statePersistenceTestableAccount(profile: Profile, constellation: DeviceConstellation, ableToRecoverFromAuthError: Boolean = false): FirefoxAccount {
+ val account = mock<FirefoxAccount>()
+ `when`(account.getProfile(anyBoolean())).thenReturn(profile)
+ `when`(account.deviceConstellation()).thenReturn(constellation)
+ `when`(account.checkAuthorizationStatus(any())).thenReturn(ableToRecoverFromAuthError)
+ `when`(account.beginOAuthFlow(any(), any())).thenReturn(testAuthFlowUrl(entrypoint = "home-menu"))
+ `when`(account.beginPairingFlow(any(), any(), any())).thenReturn(testAuthFlowUrl(entrypoint = "home-menu"))
+ `when`(account.completeOAuthFlow(anyString(), anyString())).thenReturn(true)
+ `when`(account.getCurrentDeviceId()).thenReturn("testFxaDeviceId")
+
+ return account
}
@Test
@@ -596,7 +520,7 @@ class FxaAccountManagerTest {
@Test
fun `with persisted account and profile`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
val profile = Profile(
"testUid",
@@ -667,7 +591,7 @@ class FxaAccountManagerTest {
@Test
fun `happy authentication and profile flow`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -712,7 +636,7 @@ class FxaAccountManagerTest {
@Test(expected = FxaPanicException::class)
fun `fxa panic during initDevice flow`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -740,7 +664,7 @@ class FxaAccountManagerTest {
@Test(expected = FxaPanicException::class)
fun `fxa panic during pairing flow`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(mock())
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val accountStorage = mock<AccountStorage>()
@@ -769,7 +693,7 @@ class FxaAccountManagerTest {
@Test
fun `happy pairing authentication and profile flow`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -806,7 +730,7 @@ class FxaAccountManagerTest {
@Test
fun `repeated unfinished authentication attempts succeed`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -863,7 +787,7 @@ class FxaAccountManagerTest {
@Test
fun `unhappy authentication flow`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val accountObserver: AccountObserver = mock()
@@ -911,7 +835,7 @@ class FxaAccountManagerTest {
@Test
fun `unhappy pairing authentication flow`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
val accountObserver: AccountObserver = mock()
@@ -970,7 +894,7 @@ class FxaAccountManagerTest {
@Test
fun `authentication issues are propagated via AccountObserver`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -1025,7 +949,7 @@ class FxaAccountManagerTest {
@Test
fun `authentication issues are recoverable via checkAuthorizationState`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -1072,7 +996,7 @@ class FxaAccountManagerTest {
@Test
fun `authentication recovery flow has a circuit breaker`() = runTest {
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
val profile = Profile(uid = "testUID", avatar = null, email = "test@example.com", displayName = "test profile")
@@ -1163,7 +1087,7 @@ class FxaAccountManagerTest {
@Test
fun `unhappy profile fetching flow`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -1232,7 +1156,7 @@ class FxaAccountManagerTest {
@Test
fun `profile fetching flow hit an unrecoverable auth problem`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
@@ -1292,7 +1216,7 @@ class FxaAccountManagerTest {
@Test
fun `profile fetching flow hit an unrecoverable auth problem for which we can't determine a recovery state`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.deviceConstellation()).thenReturn(constellation)
@@ -1353,7 +1277,7 @@ class FxaAccountManagerTest {
@Test
fun `profile fetching flow hit a recoverable auth problem`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
val captor = argumentCaptor<AuthType>()
@@ -1437,7 +1361,7 @@ class FxaAccountManagerTest {
@Test(expected = FxaPanicException::class)
fun `profile fetching flow hit an fxa panic, which is re-thrown`() = runTest {
val accountStorage = mock<AccountStorage>()
- val mockAccount: OAuthAccount = mock()
+ val mockAccount: FirefoxAccount = mock()
val constellation: DeviceConstellation = mock()
`when`(mockAccount.getCurrentDeviceId()).thenReturn("testDeviceId")
@@ -1564,7 +1488,7 @@ class FxaAccountManagerTest {
}
private suspend fun prepareHappyAuthenticationFlow(
- mockAccount: OAuthAccount,
+ mockAccount: FirefoxAccount,
profile: Profile,
accountStorage: AccountStorage,
accountObserver: AccountObserver,
@@ -1608,7 +1532,7 @@ class FxaAccountManagerTest {
}
private suspend fun prepareUnhappyAuthenticationFlow(
- mockAccount: OAuthAccount,
+ mockAccount: FirefoxAccount,
profile: Profile,
accountStorage: AccountStorage,
accountObserver: AccountObserver,
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt
index 6323861f0e..df37088ca7 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/manager/StateKtTest.kt
@@ -83,8 +83,8 @@ class StateKtTest {
private fun instantiateEvent(eventClassSimpleName: String): Event {
return when (eventClassSimpleName) {
"Start" -> Event.Account.Start
- "BeginPairingFlow" -> Event.Account.BeginPairingFlow("http://some.pairing.url.com", mock())
- "BeginEmailFlow" -> Event.Account.BeginEmailFlow(mock())
+ "BeginPairingFlow" -> Event.Account.BeginPairingFlow("http://some.pairing.url.com", mock(), mock())
+ "BeginEmailFlow" -> Event.Account.BeginEmailFlow(mock(), mock())
"CancelAuth" -> Event.Progress.CancelAuth
"StartedOAuthFlow" -> Event.Progress.StartedOAuthFlow("https://example.com/oauth-start")
"AuthenticationError" -> Event.Account.AuthenticationError("fxa op")
diff --git a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt
index e0d90118fa..975fd6a481 100644
--- a/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt
+++ b/mobile/android/android-components/components/service/firefox-accounts/src/test/java/mozilla/components/service/fxa/store/SyncStoreSupportTest.kt
@@ -12,13 +12,16 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
+import mozilla.components.concept.sync.AuthFlowError
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.Avatar
import mozilla.components.concept.sync.ConstellationState
import mozilla.components.concept.sync.DeviceConstellation
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
+import mozilla.components.service.fxa.manager.AccountState
import mozilla.components.service.fxa.manager.FxaAccountManager
+import mozilla.components.service.fxa.manager.SCOPE_PROFILE
import mozilla.components.support.test.any
import mozilla.components.support.test.coMock
import mozilla.components.support.test.eq
@@ -26,9 +29,11 @@ import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import java.lang.Exception
@OptIn(ExperimentalCoroutinesApi::class)
@@ -115,7 +120,7 @@ class SyncStoreSupportTest {
}
@Test
- fun `GIVEN account observer WHEN onAuthenticated observed with profile THEN account state updated`() = coroutineScope.runTest {
+ fun `GIVEN account observer WHEN onAuthenticated observed with profile THEN account and account state are updated`() = coroutineScope.runTest {
val profile = generateProfile()
val constellation = mock<DeviceConstellation>()
val account = coMock<OAuthAccount> {
@@ -125,6 +130,8 @@ class SyncStoreSupportTest {
whenever(getProfile()).thenReturn(profile)
}
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+
accountObserver.onAuthenticated(account, mock<AuthType.Existing>())
runCurrent()
@@ -138,10 +145,11 @@ class SyncStoreSupportTest {
)
store.waitUntilIdle()
assertEquals(expected, store.state.account)
+ assertEquals(AccountState.Authenticated, store.state.accountState)
}
@Test
- fun `GIVEN account observer WHEN onAuthenticated observed without profile THEN account not updated`() = coroutineScope.runTest {
+ fun `GIVEN account observer WHEN onAuthenticated observed without profile THEN account and account state are not updated`() = coroutineScope.runTest {
val constellation = mock<DeviceConstellation>()
val account = coMock<OAuthAccount> {
whenever(deviceConstellation()).thenReturn(constellation)
@@ -152,11 +160,12 @@ class SyncStoreSupportTest {
runCurrent()
store.waitUntilIdle()
- assertEquals(null, store.state.account)
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
}
@Test
- fun `GIVEN user is logged in WHEN onLoggedOut observed THEN sync status and account updated`() = coroutineScope.runTest {
+ fun `GIVEN user is logged in WHEN onLoggedOut observed THEN sync status and account states are updated`() = coroutineScope.runTest {
val account = coMock<OAuthAccount> {
whenever(deviceConstellation()).thenReturn(mock())
whenever(getProfile()).thenReturn(null)
@@ -169,7 +178,94 @@ class SyncStoreSupportTest {
store.waitUntilIdle()
assertEquals(SyncStatus.LoggedOut, store.state.status)
- assertEquals(null, store.state.account)
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onAuthenticationProblems observed THEN account state is updated`() = coroutineScope.runTest {
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+
+ accountObserver.onAuthenticationProblems()
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertEquals(AccountState.AuthenticationProblem, store.state.accountState)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onFlowError observed THEN account state is updated`() = coroutineScope.runTest {
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+
+ accountObserver.onFlowError(mock<AuthFlowError>())
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onReady observed with profile THEN account states are updated`() = coroutineScope.runTest {
+ val profile = generateProfile()
+ val currentDeviceId = "id"
+ val sessionToken = "token"
+ val constellation = mock<DeviceConstellation>()
+ val authenticatedAccount = coMock<OAuthAccount> {
+ whenever(deviceConstellation()).thenReturn(constellation)
+ whenever(getCurrentDeviceId()).thenReturn(currentDeviceId)
+ whenever(getSessionToken()).thenReturn(sessionToken)
+ whenever(getProfile()).thenReturn(profile)
+ }
+ val account = Account(
+ uid = profile.uid,
+ email = profile.email,
+ avatar = profile.avatar,
+ displayName = profile.displayName,
+ currentDeviceId = currentDeviceId,
+ sessionToken = sessionToken,
+ )
+
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+
+ `when`(authenticatedAccount.checkAuthorizationStatus(eq(SCOPE_PROFILE))).thenReturn(false)
+
+ accountObserver.onReady(authenticatedAccount = authenticatedAccount)
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertEquals(account, store.state.account)
+ assertEquals(AccountState.AuthenticationProblem, store.state.accountState)
+
+ `when`(authenticatedAccount.checkAuthorizationStatus(eq(SCOPE_PROFILE))).thenReturn(true)
+
+ accountObserver.onReady(authenticatedAccount = authenticatedAccount)
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertEquals(account, store.state.account)
+ assertEquals(AccountState.Authenticated, store.state.accountState)
+ }
+
+ @Test
+ fun `GIVEN account observer WHEN onReady observed without profile THEN account states are not updated`() = coroutineScope.runTest {
+ val constellation = mock<DeviceConstellation>()
+ val account = coMock<OAuthAccount> {
+ whenever(deviceConstellation()).thenReturn(constellation)
+ whenever(getProfile()).thenReturn(null)
+ }
+
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
+
+ accountObserver.onReady(account)
+ runCurrent()
+
+ store.waitUntilIdle()
+ assertNull(store.state.account)
+ assertEquals(AccountState.NotAuthenticated, store.state.accountState)
}
@Test