Skip to content
This repository has been archived by the owner on Jun 17, 2024. It is now read-only.

Commit

Permalink
Bug 1856729 - Switch to new app-services FxaClient
Browse files Browse the repository at this point in the history
Use the new functionality from app-services to replace code in
service-firefox-accounts

General:
- Updated code to use FxaConfig class rather than Config.  Config
  wrapped FxaConfig, but provided little value.
- Updated `handleFxaExceptions` and removed all other Utils functions
- Use FxaClient's CoroutineContext to run all fxa-related jobs.
- Many FxaClient methods no longer directly return a result, but instead
  input an optional `CompletableDeferred<T>` that they complete when the
  operation is finish.  See the app-services PR for details.

FirefoxAccount:
- Don't wrap FxaClient calls in withContext, this is handled in
  app-services now.
- Updated FirefoxAccount docs to describe its relationship to
  OAuthAccount.
- Make AccountStorage return a FirefoxAccount directly instead of an
  OAuthAccount

FxaDeviceConstellation:
- Let FxaClient handle auth errors when sending device commands
- Updating FxaDeviceSettingsCache is now handled in the
  FxaAccountManager.onFxaEvent method

FxaAccountManager:
- Replaced state machine code with the new `queueAction()` +
  `onFxaEvent` functions instead.

Tests:
  - Removed a good deal of tests, the app-services tests should replace
    these.

  - There were several tests that dealt with state persistence plus
    oauth.  I think these should be convered by the regular state change
    tests, were they testing something else?

  - Auth recovery is handled by several tests starting with
    `FxaActionProcessor calls checkAuthorizationStatus after auth
    errors`

  - The main tests I tried to keep were the ones focused on observer
    notification and other integrations with the larger code base.
    We're mostly just forwarding the events that FxaClient sends us to
    handle this.
  • Loading branch information
bendk committed Oct 16, 2023
1 parent ade75e7 commit 87bd22e
Show file tree
Hide file tree
Showing 29 changed files with 945 additions and 2,990 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ interface DeviceConstellation : Observable<AccountEventsObserver> {
* Perform actions necessary to finalize device initialization based on [authType].
* @param authType Type of an authentication event we're experiencing.
* @param config A [DeviceConfig] that describes current device.
* @return A boolean success flag.
*/
suspend fun finalizeDevice(authType: AuthType, config: DeviceConfig): ServiceResult
fun finalizeDevice(authType: AuthType, config: DeviceConfig)

/**
* Current state of the constellation. May be missing if state was never queried.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import mozilla.appservices.fxaclient.contentUrl
import mozilla.appservices.fxaclient.isCustom
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineSession
Expand All @@ -20,7 +22,6 @@ import mozilla.components.concept.engine.webextension.WebExtensionRuntime
import mozilla.components.concept.sync.AuthType
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.service.fxa.FxaAuthData
import mozilla.components.service.fxa.Server
import mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.SyncEngine
import mozilla.components.service.fxa.manager.FxaAccountManager
Expand Down Expand Up @@ -170,8 +171,12 @@ class FxaWebChannelFeature(
private val serverConfig: ServerConfig,
) : MessageHandler {
override fun onPortConnected(port: Port) {
if (Server.values().none { it.contentUrl == serverConfig.contentUrl }) {
port.postMessage(JSONObject().put("type", "overrideFxAServer").put("url", serverConfig.contentUrl))
if (serverConfig.server.isCustom()) {
port.postMessage(
JSONObject()
.put("type", "overrideFxAServer")
.put("url", serverConfig.server.contentUrl()),
)
}
}
}
Expand Down Expand Up @@ -372,7 +377,7 @@ class FxaWebChannelFeature(

private fun isCommunicationAllowed(serverConfig: ServerConfig, port: Port): Boolean {
val senderOrigin = port.senderUrl()
val expectedOrigin = serverConfig.contentUrl
val expectedOrigin = serverConfig.server.contentUrl()
return isCommunicationAllowed(senderOrigin, expectedOrigin)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,31 @@ import android.os.Looper.getMainLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.test.runTest
import mozilla.appservices.fxaclient.FxaAction
import mozilla.appservices.fxaclient.FxaServer
import mozilla.components.concept.engine.request.RequestInterceptor
import mozilla.components.concept.sync.AccountEventsObserver
import mozilla.components.concept.sync.AuthFlowUrl
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.DeviceConfig
import mozilla.components.concept.sync.DeviceType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.FxAEntryPoint
import mozilla.components.concept.sync.Profile
import mozilla.components.service.fxa.FirefoxAccount
import mozilla.components.service.fxa.FxaAuthData
import mozilla.components.service.fxa.Server
import mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.StorageWrapper
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.support.base.observer.ObserverRegistry
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 mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
Expand All @@ -43,13 +45,9 @@ internal class TestableStorageWrapper(
manager: FxaAccountManager,
accountEventObserverRegistry: ObserverRegistry<AccountEventsObserver>,
serverConfig: ServerConfig,
private val block: () -> OAuthAccount = {
val account: OAuthAccount = mock()
`when`(account.deviceConstellation()).thenReturn(mock())
account
},
val mockAccount: FirefoxAccount,
) : StorageWrapper(manager, accountEventObserverRegistry, serverConfig) {
override fun obtainAccount(): OAuthAccount = block()
override fun obtainAccount(): FirefoxAccount = mockAccount
}

// Same as the actual account manager, except we get to control how FirefoxAccountShaped instances
Expand All @@ -61,14 +59,20 @@ class TestableFxaAccountManager(
config: ServerConfig,
scopes: Set<String>,
coroutineContext: CoroutineContext,
block: () -> OAuthAccount = { mock() },
) : FxaAccountManager(context, config, DeviceConfig("test", DeviceType.MOBILE, setOf()), null, scopes, null, coroutineContext) {
private val testableStorageWrapper = TestableStorageWrapper(this, accountEventObserverRegistry, serverConfig, block)
) : FxaAccountManager(context, config, DeviceConfig("test", DeviceType.MOBILE, setOf()), null, scopes, null) {
val mockAccount: FirefoxAccount = mock<FirefoxAccount>().also {
whenever(it.clientCoroutineContext).thenReturn(coroutineContext)
}
private val testableStorageWrapper = TestableStorageWrapper(this, accountEventObserverRegistry, serverConfig, mockAccount)
override fun getStorageWrapper(): StorageWrapper {
return testableStorageWrapper
}
}

val entryPoint: FxAEntryPoint = mock<FxAEntryPoint>().apply {
whenever(entryName).thenReturn("home-menu")
}

@RunWith(AndroidJUnit4::class)
class FirefoxAccountsAuthFeatureTest {
// Note that tests that involve secure storage specify API=21, because of issues testing secure storage on
Expand All @@ -88,9 +92,9 @@ class FirefoxAccountsAuthFeatureTest {
) { _, url ->
authUrl.complete(url)
}
feature.beginAuthentication(testContext, mock())
feature.beginAuthentication(testContext, entryPoint)
authUrl.await()
assertEquals("auth://url", authUrl.getCompleted())
assertEquals("auth://url?state=test-state", authUrl.getCompleted())
}

@Config(sdk = [22])
Expand All @@ -107,9 +111,9 @@ class FirefoxAccountsAuthFeatureTest {
) { _, url ->
authUrl.complete(url)
}
feature.beginPairingAuthentication(testContext, "auth://pair", mock())
feature.beginPairingAuthentication(testContext, "auth://pair", entryPoint)
authUrl.await()
assertEquals("auth://url", authUrl.getCompleted())
assertEquals("auth://url?state=test-state", authUrl.getCompleted())
}

@Config(sdk = [22])
Expand All @@ -127,7 +131,7 @@ class FirefoxAccountsAuthFeatureTest {
) { _, url ->
authUrl.complete(url)
}
feature.beginAuthentication(testContext, mock())
feature.beginAuthentication(testContext, entryPoint)
authUrl.await()
// Fallback url is invoked.
assertEquals("https://accounts.firefox.com/signin", authUrl.getCompleted())
Expand All @@ -148,7 +152,7 @@ class FirefoxAccountsAuthFeatureTest {
) { _, url ->
authUrl.complete(url)
}
feature.beginPairingAuthentication(testContext, "auth://pair", mock())
feature.beginPairingAuthentication(testContext, "auth://pair", entryPoint)
authUrl.await()
// Fallback url is invoked.
assertEquals("https://accounts.firefox.com/signin", authUrl.getCompleted())
Expand Down Expand Up @@ -246,24 +250,26 @@ class FirefoxAccountsAuthFeatureTest {
private suspend fun prepareAccountManagerForSuccessfulAuthentication(
coroutineContext: CoroutineContext,
): TestableFxaAccountManager {
val mockAccount: OAuthAccount = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "[email protected]", displayName = "test profile")

`when`(mockAccount.deviceConstellation()).thenReturn(mock())
`when`(mockAccount.getProfile(anyBoolean())).thenReturn(profile)
`when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(AuthFlowUrl("authState", "auth://url"))
`when`(mockAccount.beginPairingFlow(anyString(), any(), any())).thenReturn(AuthFlowUrl("authState", "auth://url"))
`when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)

val manager = TestableFxaAccountManager(
testContext,
ServerConfig(Server.RELEASE, "dummyId", "bad://url"),
ServerConfig(FxaServer.Release, "dummyId", "bad://url", null),
setOf("test-scope"),
coroutineContext,
) {
mockAccount
)
manager.mockAccount.let {
`when`(it.deviceConstellation()).thenReturn(mock())
`when`(it.getProfile(anyBoolean())).thenReturn(profile)
val captor = argumentCaptor<FxaAction>()
`when`(it.queueAction(captor.capture())).thenAnswer {
val action = captor.value
when (action) {
is FxaAction.BeginOAuthFlow -> action.result?.complete("auth://url?state=test-state")
is FxaAction.BeginPairingFlow -> action.result?.complete("auth://url?state=test-state")
else -> Unit
}
}
}

manager.start()

return manager
Expand All @@ -273,24 +279,26 @@ class FirefoxAccountsAuthFeatureTest {
private suspend fun prepareAccountManagerForFailedAuthentication(
coroutineContext: CoroutineContext,
): TestableFxaAccountManager {
val mockAccount: OAuthAccount = mock()
val profile = Profile(uid = "testUID", avatar = null, email = "[email protected]", displayName = "test profile")

`when`(mockAccount.getProfile(anyBoolean())).thenReturn(profile)
`when`(mockAccount.deviceConstellation()).thenReturn(mock())
`when`(mockAccount.beginOAuthFlow(any(), any())).thenReturn(null)
`when`(mockAccount.beginPairingFlow(anyString(), any(), any())).thenReturn(null)
`when`(mockAccount.completeOAuthFlow(anyString(), anyString())).thenReturn(true)

val manager = TestableFxaAccountManager(
testContext,
ServerConfig(Server.RELEASE, "dummyId", "bad://url"),
ServerConfig(FxaServer.Release, "dummyId", "bad://url", null),
setOf("test-scope"),
coroutineContext,
) {
mockAccount
)
manager.mockAccount.let {
`when`(it.deviceConstellation()).thenReturn(mock())
`when`(it.getProfile(anyBoolean())).thenReturn(profile)
val captor = argumentCaptor<FxaAction>()
`when`(it.queueAction(captor.capture())).thenAnswer {
val action = captor.value
when (action) {
is FxaAction.BeginOAuthFlow -> action.result?.complete(null)
is FxaAction.BeginPairingFlow -> action.result?.complete(null)
else -> Unit
}
}
}

manager.start()

return manager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package mozilla.components.feature.accounts
import android.os.Looper.getMainLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.test.runTest
import mozilla.appservices.fxaclient.FxaServer
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
Expand All @@ -19,7 +20,6 @@ import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.concept.sync.Profile
import mozilla.components.service.fxa.FxaAuthData
import mozilla.components.service.fxa.Server
import mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.SyncEngine
import mozilla.components.service.fxa.manager.FxaAccountManager
Expand Down Expand Up @@ -107,7 +107,7 @@ class FxaWebChannelFeatureTest {
val controller: WebExtensionController = mock()
val webchannelFeature = FxaWebChannelFeature(null, engine, store, accountManager, serverConfig)

whenever(serverConfig.contentUrl).thenReturn("https://foo.bar")
whenever(serverConfig.server).thenReturn(FxaServer.Custom("https://foo.bar"))
webchannelFeature.extensionController = controller

webchannelFeature.start()
Expand Down Expand Up @@ -135,7 +135,7 @@ class FxaWebChannelFeatureTest {
val controller: WebExtensionController = mock()
val webchannelFeature = FxaWebChannelFeature(null, engine, store, accountManager, serverConfig)

whenever(serverConfig.contentUrl).thenReturn(Server.RELEASE.contentUrl)
whenever(serverConfig.server).thenReturn(FxaServer.Release)
webchannelFeature.extensionController = controller

webchannelFeature.start()
Expand Down Expand Up @@ -825,7 +825,7 @@ class FxaWebChannelFeatureTest {
whenever(accountManager.supportedSyncEngines()).thenReturn(expectedEngines)
whenever(port.engineSession).thenReturn(engineSession)
whenever(port.senderUrl()).thenReturn("https://foo.bar/email")
whenever(serverConfig.contentUrl).thenReturn("https://foo.bar")
whenever(serverConfig.server).thenReturn(FxaServer.Custom("https://foo.bar"))

return spy(FxaWebChannelFeature(null, mock(), store, accountManager, serverConfig, fxaCapabilities))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import androidx.annotation.VisibleForTesting
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
Expand All @@ -25,16 +24,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
}

/**
Expand Down Expand Up @@ -79,16 +78,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)
}

/**
Expand All @@ -109,7 +108,7 @@ internal class AccountEventsIntegration(

internal interface AccountStorage {
@Throws(Exception::class)
fun read(): OAuthAccount?
fun read(): FirefoxAccount?
fun write(accountState: String)
fun clear()
}
Expand Down Expand Up @@ -155,7 +154,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

Expand Down Expand Up @@ -233,7 +232,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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ package mozilla.components.service.fxa

import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider

typealias ServerConfig = mozilla.appservices.fxaclient.Config
typealias Server = mozilla.appservices.fxaclient.Config.Server
typealias ServerConfig = mozilla.appservices.fxaclient.FxaConfig
typealias Server = mozilla.appservices.fxaclient.FxaServer

/**
* @property periodMinutes How frequently periodic sync should happen.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ typealias FxaPanicException = mozilla.appservices.fxaclient.FxaException.Panic
*/
typealias FxaUnauthorizedException = mozilla.appservices.fxaclient.FxaException.Authentication

/**
* Thrown when an access token is missing a required scoped key
*/
typealias FxaSyncScopedKeyMissingException = mozilla.appservices.fxaclient.FxaException.SyncScopedKeyMissing

/**
* Thrown when the Rust library hits an unexpected error that isn't a panic.
* This may indicate library misuse, network errors, etc.
Expand Down
Loading

1 comment on commit 87bd22e

@firefoxci-taskcluster
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh oh! Looks like an error! Details

Failed to fetch task artifact public/github/customCheckRunText.md for GitHub integration.
Make sure the artifact exists on the worker or other location.

Please sign in to comment.