diff --git a/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaActionProcessor.kt b/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaActionProcessor.kt index 099cd66304..d9d7e59363 100644 --- a/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaActionProcessor.kt +++ b/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaActionProcessor.kt @@ -66,29 +66,46 @@ internal class FxaActionProcessor( @Suppress("ComplexMethod") internal suspend fun processAction(action: FxaAction) { - val currentState = currentState - // Match on the current state since many actions are only valid from a particular state - when (currentState) { - is FxaAuthState.Connected -> when (action) { - is FxaAction.Disconnect -> handleDisconnect(action) - FxaAction.CheckAuthorization -> handleCheckAuthorization(currentState) + val isValid = when (action) { + // Auth flow actions are valid if you're disconnected and also if you're already + // authenticating. If a consumer accidentally starts multiple flows we should not + // create extra issues for them. + is FxaAction.BeginOAuthFlow, + is FxaAction.BeginPairingFlow, + is FxaAction.CompleteOAuthFlow, + is FxaAction.CancelOAuthFlow, + -> currentState in listOf(FxaAuthState.DISCONNECTED, FxaAuthState.AUTHENTICATING) + // These actions require the user to be connected + FxaAction.CheckAuthorization, + is FxaAction.InitializeDevice, + is FxaAction.EnsureCapabilities, + is FxaAction.SetDeviceName, + is FxaAction.SetDevicePushSubscription, + is FxaAction.SendSingleTab, + -> currentState.isConnected() + // These are always valid, although they're no-op if you're already in the + // DISCONNECTED/AUTH_ISSUES state + FxaAction.Disconnect, + FxaAction.LogoutFromAuthIssues, + -> true + } + if (isValid) { + when (action) { + is FxaAction.BeginOAuthFlow -> handleBeginOAuthFlow(action) + is FxaAction.BeginPairingFlow -> handleBeginPairingFlow(action) + is FxaAction.CompleteOAuthFlow -> handleCompleteFlow(action) + is FxaAction.CancelOAuthFlow -> handleCancelFlow() is FxaAction.InitializeDevice -> handleInitializeDevice(action) is FxaAction.EnsureCapabilities -> handleEnsureCapabilities(action) is FxaAction.SetDeviceName -> handleSetDeviceName(action) is FxaAction.SetDevicePushSubscription -> handleSetDevicePushSubscription(action) is FxaAction.SendSingleTab -> handleSendSingleTab(action) - else -> Log.e(LOG_TAG, "Invalid $action (state: $currentState)") - } - is FxaAuthState.Disconnected -> when (action) { - is FxaAction.BeginOAuthFlow -> handleBeginOAuthFlow(currentState, action) - is FxaAction.BeginPairingFlow -> handleBeginPairingFlow(currentState, action) - is FxaAction.CompleteOAuthFlow -> handleCompleteFlow(currentState, action) - is FxaAction.CancelOAuthFlow -> handleCancelFlow(currentState) - // If we see Disconnect or CheckAuthorization from the Disconnected state, just ignore it - FxaAction.CheckAuthorization -> Unit - is FxaAction.Disconnect -> Unit - else -> Log.e(LOG_TAG, "Invalid $action (state: $currentState)") + FxaAction.CheckAuthorization -> handleCheckAuthorization() + FxaAction.Disconnect -> handleDisconnect() + FxaAction.LogoutFromAuthIssues -> handleLogoutFromAuthIssues() } + } else { + Log.e(LOG_TAG, "Invalid $action (state: $currentState)") } } @@ -102,10 +119,12 @@ internal class FxaActionProcessor( } } - private suspend fun changeState(newState: FxaAuthState, transition: FxaAuthStateTransition) { - Log.d(LOG_TAG, "Changing state from $currentState to $newState") - currentState = newState - sendEvent(FxaEvent.AuthStateChanged(newState, transition)) + private suspend fun sendAuthEvent(kind: FxaAuthEventKind, newState: FxaAuthState?) { + if (newState != null && newState != currentState) { + Log.d(LOG_TAG, "Changing state from $currentState to $newState") + currentState = newState + } + sendEvent(FxaEvent.AuthEvent(kind, currentState)) } // Perform an operation, retrying after network errors @@ -136,23 +155,21 @@ internal class FxaActionProcessor( try { return withNetworkRetry(operation) } catch (e: FxaException.Authentication) { - val currentState = currentState - - if (currentState !is FxaAuthState.Connected) { + if (!currentState.isConnected()) { throw e } if (retryLogic.shouldRecheckAuthStatus()) { Log.d(LOG_TAG, "Auth error: re-checking") - handleCheckAuthorization(currentState) + handleCheckAuthorization() } else { Log.d(LOG_TAG, "Auth error: disconnecting") - inner.disconnectFromAuthIssues() + inner.logoutFromAuthIssues() persistState() - changeState(FxaAuthState.Disconnected(true), FxaAuthStateTransition.AUTH_CHECK_FAILED) + sendAuthEvent(FxaAuthEventKind.AUTH_CHECK_FAILED, FxaAuthState.AUTH_ISSUES) } - if (this.currentState is FxaAuthState.Connected) { + if (currentState.isConnected()) { continue } else { throw e @@ -161,49 +178,53 @@ internal class FxaActionProcessor( } } - private suspend fun handleBeginOAuthFlow(currentState: FxaAuthState.Disconnected, action: FxaAction.BeginOAuthFlow) { - handleBeginEitherOAuthFlow(currentState, action.result) { + private suspend fun handleBeginOAuthFlow(action: FxaAction.BeginOAuthFlow) { + handleBeginEitherOAuthFlow(action.result) { inner.beginOauthFlow(action.scopes.toList(), action.entrypoint, MetricsParams(mapOf())) } } - private suspend fun handleBeginPairingFlow(currentState: FxaAuthState.Disconnected, action: FxaAction.BeginPairingFlow) { - handleBeginEitherOAuthFlow(currentState, action.result) { + private suspend fun handleBeginPairingFlow(action: FxaAction.BeginPairingFlow) { + handleBeginEitherOAuthFlow(action.result) { inner.beginPairingFlow(action.pairingUrl, action.scopes.toList(), action.entrypoint, MetricsParams(mapOf())) } } - private suspend fun handleBeginEitherOAuthFlow(currentState: FxaAuthState.Disconnected, result: CompletableDeferred?, operation: () -> String) { + private suspend fun handleBeginEitherOAuthFlow(result: CompletableDeferred?, operation: () -> String) { try { val url = withRetry { operation() } persistState() - changeState(currentState.copy(connecting = true), FxaAuthStateTransition.OAUTH_STARTED) + sendAuthEvent(FxaAuthEventKind.OAUTH_STARTED, FxaAuthState.AUTHENTICATING) sendEvent(FxaEvent.BeginOAuthFlow(url)) result?.complete(url) } catch (e: FxaException) { Log.e(LOG_TAG, "Exception when handling BeginOAuthFlow", e) persistState() - changeState(currentState.copy(connecting = false), FxaAuthStateTransition.OAUTH_FAILED_TO_BEGIN) + // Stay in the AUTHENTICATING if we were in that state , since there may be another + // oauth flow in progress. We only switch to DISCONNECTED if we see CancelOAuthFlow. + sendAuthEvent(FxaAuthEventKind.OAUTH_FAILED_TO_BEGIN, null) result?.complete(null) } } - private suspend fun handleCompleteFlow(currentState: FxaAuthState.Disconnected, action: FxaAction.CompleteOAuthFlow) { + private suspend fun handleCompleteFlow(action: FxaAction.CompleteOAuthFlow) { try { withRetry { inner.completeOauthFlow(action.code, action.state) } persistState() - changeState(FxaAuthState.Connected(), FxaAuthStateTransition.OAUTH_COMPLETE) + sendAuthEvent(FxaAuthEventKind.OAUTH_COMPLETE, FxaAuthState.CONNECTED) } catch (e: FxaException) { persistState() Log.e(LOG_TAG, "Exception when handling CompleteOAuthFlow", e) - changeState(currentState, FxaAuthStateTransition.OAUTH_FAILED_TO_COMPLETE) + // Stay in the AUTHENTICATING, since there may be another oauth flow in progress. We + // only switch to DISCONNECTED if we see CancelOAuthFlow. + sendAuthEvent(FxaAuthEventKind.OAUTH_FAILED_TO_COMPLETE, null) } } - private suspend fun handleCancelFlow(currentState: FxaAuthState.Disconnected) { + private suspend fun handleCancelFlow() { // No need to call an inner method or persist the state, since the connecting flag is - // handled soley in this layer - changeState(currentState.copy(connecting = false), FxaAuthStateTransition.OAUTH_CANCELLED) + // handled in this layer only. + sendAuthEvent(FxaAuthEventKind.OAUTH_CANCELLED, FxaAuthState.DISCONNECTED) } private suspend fun handleInitializeDevice(action: FxaAction.InitializeDevice) { @@ -257,20 +278,29 @@ internal class FxaActionProcessor( } } - private suspend fun handleDisconnect(action: FxaAction.Disconnect) { - if (action.fromAuthIssues) { - inner.disconnectFromAuthIssues() - persistState() - changeState(FxaAuthState.Disconnected(fromAuthIssues = true), FxaAuthStateTransition.AUTH_CHECK_FAILED) - } else { - inner.disconnect() - persistState() - changeState(FxaAuthState.Disconnected(), FxaAuthStateTransition.DISCONNECTED) + private suspend fun handleDisconnect() { + if (currentState == FxaAuthState.DISCONNECTED) { + return + } + inner.disconnect() + persistState() + sendAuthEvent(FxaAuthEventKind.DISCONNECTED, FxaAuthState.DISCONNECTED) + } + + private suspend fun handleLogoutFromAuthIssues() { + if (currentState in listOf(FxaAuthState.AUTH_ISSUES, FxaAuthState.DISCONNECTED)) { + return } + inner.logoutFromAuthIssues() + persistState() + sendAuthEvent(FxaAuthEventKind.LOGOUT_FROM_AUTH_ISSUES, FxaAuthState.AUTH_ISSUES) } - private suspend fun handleCheckAuthorization(currentState: FxaAuthState.Connected) { - changeState(currentState.copy(authCheckInProgress = true), FxaAuthStateTransition.AUTH_CHECK_STARTED) + private suspend fun handleCheckAuthorization() { + if (currentState in listOf(FxaAuthState.DISCONNECTED, FxaAuthState.AUTH_ISSUES)) { + return + } + sendAuthEvent(FxaAuthEventKind.AUTH_CHECK_STARTED, FxaAuthState.CHECKING_AUTH) val success = try { val status = withNetworkRetry { inner.checkAuthorizationStatus() } status.active @@ -287,11 +317,11 @@ internal class FxaActionProcessor( } if (success) { persistState() - changeState(currentState.copy(authCheckInProgress = false), FxaAuthStateTransition.AUTH_CHECK_SUCCESS) + sendAuthEvent(FxaAuthEventKind.AUTH_CHECK_SUCCESS, FxaAuthState.CONNECTED) } else { - inner.disconnectFromAuthIssues() + inner.logoutFromAuthIssues() persistState() - changeState(FxaAuthState.Disconnected(true), FxaAuthStateTransition.AUTH_CHECK_FAILED) + sendAuthEvent(FxaAuthEventKind.AUTH_CHECK_FAILED, FxaAuthState.AUTH_ISSUES) } } } diff --git a/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt b/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt index 22123a5a49..fb0ff4792e 100644 --- a/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt +++ b/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/FxaClient.kt @@ -278,7 +278,7 @@ class FxaClient private constructor( /** * Saves the current account's authentication state as a JSON string, for persistence in * the Android KeyStore/shared preferences. The authentication state can be restored using - * [FirefoxAccount.fromJSONString]. + * [FirefoxAccount.fromJson]. * * FIXME: https://github.com/mozilla/application-services/issues/5819 * diff --git a/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/StateTypes.kt b/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/StateTypes.kt index 1ac9009274..222b41817c 100644 --- a/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/StateTypes.kt +++ b/components/fxa-client/android/src/main/java/mozilla/appservices/fxaclient/StateTypes.kt @@ -31,6 +31,11 @@ sealed class FxaAction { /** * Begin an OAuth flow * + * BeginOAuthFlow can be sent in either the DISCONNECTED or AUTHENTICATING state. If the + * operation fails the state will not change. AuthEvent(OAUTH_FAILED_TO_BEGIN) will be sent to + * the event handler, which can respond by sending CancelOAuthFlow if the application wants to + * move back to the DISCONNECTED state. + * * @param scopes OAuth scopes to request * @param entrypoint OAuth entrypoint * @param result If present, will be completed with the OAuth URL to navigate users too @@ -44,6 +49,11 @@ sealed class FxaAction { /** * Begin an OAuth flow using a paring code URL * + * BeginPairingFlow can be sent in either the DISCONNECTED or AUTHENTICATING state. If the + * operation fails the state will not change. AuthEvent(OAUTH_FAILED_TO_BEGIN) will be sent to + * the event handler, which can respond by sending CancelOAuthFlow if the application wants to + * move back to the DISCONNECTED state. + * * @param pairingUrl the url to initialize the paring flow with * @param scopes OAuth scopes to request * @param entrypoint OAuth entrypoint @@ -143,12 +153,16 @@ sealed class FxaAction { /** * Disconnect from the FxA server and destroy our device record. + */ + object Disconnect : FxaAction() + + /** + * Logout because of authentication / authorization issues * - * @param fromAuthIssues: are we disconnecting because of auth issues? Setting this flag - * changes `FxaEvent.AuthStateChanged` so that the `fromAuthIssues` flag is will set and the - * transition is `AUTH_CHECK_FAILED` + * Send this action if the user has gotten into a unathorized state without logging out, for + * example because of a password reset. The sure will need to re-authenticate to login. */ - data class Disconnect(val fromAuthIssues: Boolean = false) : FxaAction() + object LogoutFromAuthIssues : FxaAction() /** * Check the FxA authorization status. @@ -164,11 +178,15 @@ sealed class FxaAction { */ sealed class FxaEvent { /** - * Called when the auth state changes. Applications should use this to update their UI. + * Sent on login, logout, auth checks, etc. See [FxaAuthEventKind] for a list of all auth events. + * `state` is the current auth state of the client. All auth state transitions are accompanied + * by an AuthEvent, although some AuthEvents don't correspond to a state transition. + * + * Applications should use this to update their UI. */ - data class AuthStateChanged( - val newState: FxaAuthState, - val transition: FxaAuthStateTransition, + data class AuthEvent( + val kind: FxaAuthEventKind, + val state: FxaAuthState, ) : FxaEvent() /** @@ -196,43 +214,30 @@ sealed class FxaEvent { /** * Kotlin authorization state class * - * This is [FxaRustAuthState] with added data that Rust doesn't track yet. + * This is [FxaRustAuthState] with added states that Rust doesn't track yet. */ -sealed class FxaAuthState { - /** - * Client has disconnected - * - * @property fromAuthIssues client was disconnected because of invalid auth tokens, for - * example because of a password reset on another device - * @property connecting is there an OAuth flow in progress? - */ - data class Disconnected( - val fromAuthIssues: Boolean = false, - val connecting: Boolean = false, - ) : FxaAuthState() +enum class FxaAuthState { + DISCONNECTED, + AUTHENTICATING, + CONNECTED, + CHECKING_AUTH, + AUTH_ISSUES, + ; - /** - * Client is currently connected - * - * @property authCheckInProgress Client is checking the auth tokens and may disconnect soon - */ - data class Connected( - val authCheckInProgress: Boolean = false, - ) : FxaAuthState() + fun isConnected() = (this == CONNECTED) companion object { fun fromRust(authState: FxaRustAuthState): FxaAuthState { return when (authState) { - is FxaRustAuthState.Connected -> FxaAuthState.Connected() - is FxaRustAuthState.Disconnected -> { - FxaAuthState.Disconnected(authState.fromAuthIssues) - } + FxaRustAuthState.CONNECTED -> FxaAuthState.CONNECTED + FxaRustAuthState.DISCONNECTED -> FxaAuthState.DISCONNECTED + FxaRustAuthState.AUTH_ISSUES -> FxaAuthState.AUTH_ISSUES } } } } -enum class FxaAuthStateTransition { +enum class FxaAuthEventKind { OAUTH_STARTED, OAUTH_COMPLETE, OAUTH_CANCELLED, @@ -242,6 +247,8 @@ enum class FxaAuthStateTransition { AUTH_CHECK_STARTED, AUTH_CHECK_FAILED, AUTH_CHECK_SUCCESS, + // This is sent back when the consumer sends the `LogoutFromAuthIssues` action + LOGOUT_FROM_AUTH_ISSUES, } enum class FxaDeviceOperation { diff --git a/components/fxa-client/android/src/test/java/mozilla/appservices/fxaclient/FxaActionProcessorTest.kt b/components/fxa-client/android/src/test/java/mozilla/appservices/fxaclient/FxaActionProcessorTest.kt index ac5b502583..5afc6b0935 100644 --- a/components/fxa-client/android/src/test/java/mozilla/appservices/fxaclient/FxaActionProcessorTest.kt +++ b/components/fxa-client/android/src/test/java/mozilla/appservices/fxaclient/FxaActionProcessorTest.kt @@ -45,58 +45,60 @@ val setDeviceNameAction = FxaAction.SetDeviceName("My Phone") val setDevicePushSubscriptionAction = FxaAction.SetDevicePushSubscription("endpoint", "public-key", "auth-key") val sendSingleTabAction = FxaAction.SendSingleTab("my-other-device", "My page", "http://example.com/sent-tab") +fun mockFirefoxAccount() = mockk(relaxed = true).apply { + every { getAuthState() } returns FxaRustAuthState.DISCONNECTED + every { beginOauthFlow(any(), any(), any()) } returns "http://example.com/oauth-flow-start" + every { beginPairingFlow(any(), any(), any(), any()) } returns "http://example.com/pairing-flow-start" + every { initializeDevice(any(), any(), any()) } returns testLocalDevice + every { ensureCapabilities(any()) } returns testLocalDevice + every { setDeviceName(any()) } returns testLocalDevice + every { setPushSubscription(any()) } returns testLocalDevice + every { checkAuthorizationStatus() } returns AuthorizationInfo(active = true) +} + +fun mockThrowingFirefoxAccount() = mockk(relaxed = true).apply { + every { beginOauthFlow(any(), any(), any()) } throws testException + every { beginPairingFlow(any(), any(), any(), any()) } throws testException + every { completeOauthFlow(any(), any()) } throws testException + every { disconnect() } throws testException + every { initializeDevice(any(), any(), any()) } throws testException + every { ensureCapabilities(any()) } throws testException + every { setDeviceName(any()) } throws testException + every { setPushSubscription(any()) } throws testException + every { checkAuthorizationStatus() } throws testException + every { sendSingleTab(any(), any(), any()) } throws testException +} + +fun mockEventHandler() = mockk(relaxed = true) + +fun mockPersistState() = mockk<() -> Unit>(relaxed = true, name = "persistState") + +typealias VerifyFunc = suspend (FxaAuthState, FirefoxAccount, FxaEventHandler, () -> Unit) -> FxaAuthState + internal data class Mocks( val firefoxAccount: FirefoxAccount, val eventHandler: FxaEventHandler, val persistState: () -> Unit, val actionProcessor: FxaActionProcessor, ) { - // Verify the effects of an action + // Verify the effects of processAction // - // verifyBlock should verify all mock interactions, than return the expect new state of the - // actionProcessor. Pass in null (AKA NoEffects) to verify that there were no mock interactions - // and the state didn't change - suspend fun verifyAction(action: FxaAction, verifyBlock: ActionVerifier) { - if (verifyBlock == NeverHappens) { - return - } + // verifyBlock should verify all mock interactions, then return the expect new state of the actionProcessor. + suspend fun verifyAction(action: FxaAction, verifyFunc: VerifyFunc) { val initialState = actionProcessor.currentState actionProcessor.processAction(action) - when (verifyBlock) { - NoEffects -> assertEquals(initialState, actionProcessor.currentState) - else -> { - val expectedState = verifyBlock(firefoxAccount, eventHandler, persistState) - assertEquals(expectedState, actionProcessor.currentState) - } - } + val expectedState = verifyFunc(initialState, firefoxAccount, eventHandler, persistState) confirmVerified(firefoxAccount, eventHandler, persistState) + assertEquals(actionProcessor.currentState, expectedState) clearMocks(firefoxAccount, eventHandler, persistState, answers = false, recordedCalls = true, verificationMarks = true) } companion object { fun create(initialState: FxaAuthState, throwing: Boolean = false): Mocks { - val firefoxAccount = mockk(relaxed = true).apply { - if (!throwing) { - every { getAuthState() } returns FxaRustAuthState.Disconnected(fromAuthIssues = false) - every { beginOauthFlow(any(), any(), any()) } returns "http://example.com/oauth-flow-start" - every { beginPairingFlow(any(), any(), any(), any()) } returns "http://example.com/pairing-flow-start" - every { initializeDevice(any(), any(), any()) } returns testLocalDevice - every { ensureCapabilities(any()) } returns testLocalDevice - every { setDeviceName(any()) } returns testLocalDevice - every { setPushSubscription(any()) } returns testLocalDevice - every { checkAuthorizationStatus() } returns AuthorizationInfo(active = true) - } else { - every { beginOauthFlow(any(), any(), any()) } throws testException - every { beginPairingFlow(any(), any(), any(), any()) } throws testException - every { completeOauthFlow(any(), any()) } throws testException - every { disconnect() } throws testException - every { initializeDevice(any(), any(), any()) } throws testException - every { ensureCapabilities(any()) } throws testException - every { setDeviceName(any()) } throws testException - every { setPushSubscription(any()) } throws testException - every { checkAuthorizationStatus() } throws testException - every { sendSingleTab(any(), any(), any()) } throws testException - } + val firefoxAccount = if (throwing) { + mockThrowingFirefoxAccount() + } else { + mockFirefoxAccount() } val eventHandler = mockk(relaxed = true) val persistState = mockk<() -> Unit>(relaxed = true, name = "tryPersistState") @@ -104,40 +106,35 @@ internal data class Mocks( return Mocks(firefoxAccount, eventHandler, persistState, actionProcessor) } - // Check the effects processAction() calls for each possible state - // - // This checks each combination of: - // - connected / disconnected + // Verify the effects processAction() for all combinations of: + // - Each possible state // - Rust returns Ok() / Err() - // - // Note: this doesn't differentiate based on the FxaAuthState fields values, like - // `fromAuthIssues`. FxaActionProcessor sets these fields, but otherwise ignores them. @Suppress("LongParameterList") - internal suspend fun verifyAction( + suspend fun verifyAll( action: FxaAction, - whenDisconnected: ActionVerifier, - whenDisconnectedIfThrows: ActionVerifier, - whenConnected: ActionVerifier, - whenConnectedIfThrows: ActionVerifier, - initialStateDisconnected: FxaAuthState = FxaAuthState.Disconnected(), - initialStateConnected: FxaAuthState = FxaAuthState.Connected(), + statesWhereTheActionShouldRun: List, + verify: VerifyFunc, + verifyWhenThrows: VerifyFunc, ) { - create(initialStateDisconnected, false).verifyAction(action, whenDisconnected) - create(initialStateDisconnected, true).verifyAction(action, whenDisconnectedIfThrows) - create(initialStateConnected, false).verifyAction(action, whenConnected) - create(initialStateConnected, true).verifyAction(action, whenConnectedIfThrows) + for (state in FxaAuthState.values()) { + println("verifying for $state") + if (statesWhereTheActionShouldRun.contains(state)) { + create(state, false).verifyAction(action, verify) + if (verifyWhenThrows != NeverThrows) { + create(state, true).verifyAction(action, verifyWhenThrows) + } + } else { + // If the action shouldn't run don't coVerify any actions and check that the final state + // is the same as the initial state + create(state, false).verifyAction(action, { _, _, _, _ -> state }) + } + } } } } -typealias ActionVerifier = suspend (FirefoxAccount, FxaEventHandler, () -> Unit) -> FxaAuthState - -// Used when an action should have no effects at all -val NoEffects: ActionVerifier = { _, _, _ -> FxaAuthState.Disconnected() } - -// Used when an action should never happen, for example FxaAction.Disconnect should never throw, so -// we use this rather than making a verifier to test an impossible pathe -val NeverHappens: ActionVerifier = { _, _, _ -> FxaAuthState.Disconnected() } +// Special case for verifyAll to indicate that an action will never throw +val NeverThrows: VerifyFunc = { _, _, _, _ -> FxaAuthState.CONNECTED } /** * This is the main unit test for the FxaActionProcessor. The goal here is to take every action and @@ -163,16 +160,17 @@ val NeverHappens: ActionVerifier = { _, _, _ -> FxaAuthState.Disconnected() } class FxaActionProcessorTest { @Test fun `FxaActionProcessor handles BeginOAuthFlow`() = runTest { - Mocks.verifyAction( + Mocks.verifyAll( beginOAuthFlowAction, - whenDisconnected = { inner, eventHandler, persistState -> + listOf(FxaAuthState.DISCONNECTED, FxaAuthState.AUTHENTICATING), + { _, inner, eventHandler, persistState -> coVerifySequence { inner.beginOauthFlow(listOf("scope1"), "test-entrypoint", any()) persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(connecting = true), - transition = FxaAuthStateTransition.OAUTH_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.OAUTH_STARTED, + FxaAuthState.AUTHENTICATING, ), ) eventHandler.onFxaEvent( @@ -181,38 +179,37 @@ class FxaActionProcessorTest { ), ) } - FxaAuthState.Disconnected(connecting = true) + FxaAuthState.AUTHENTICATING }, - whenDisconnectedIfThrows = { inner, eventHandler, persistState -> + { initialState, inner, eventHandler, persistState -> coVerifySequence { inner.beginOauthFlow(listOf("scope1"), "test-entrypoint", any()) persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(), - transition = FxaAuthStateTransition.OAUTH_FAILED_TO_BEGIN, + FxaEvent.AuthEvent( + FxaAuthEventKind.OAUTH_FAILED_TO_BEGIN, + initialState, ), ) } - FxaAuthState.Disconnected() + initialState }, - whenConnected = NoEffects, - whenConnectedIfThrows = NeverHappens, ) } @Test fun `FxaActionProcessor handles BeginPairingFlow`() = runTest { - Mocks.verifyAction( + Mocks.verifyAll( beginPairingFlowAction, - whenDisconnected = { inner, eventHandler, persistState -> + listOf(FxaAuthState.DISCONNECTED, FxaAuthState.AUTHENTICATING), + { _, inner, eventHandler, persistState -> coVerifySequence { inner.beginPairingFlow("http:://example.com/pairing", listOf("scope1"), "test-entrypoint", any()) persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(connecting = true), - transition = FxaAuthStateTransition.OAUTH_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.OAUTH_STARTED, + FxaAuthState.AUTHENTICATING, ), ) eventHandler.onFxaEvent( @@ -221,45 +218,43 @@ class FxaActionProcessorTest { ), ) } - FxaAuthState.Disconnected(connecting = true) + FxaAuthState.AUTHENTICATING }, - whenDisconnectedIfThrows = { inner, eventHandler, persistState -> + { initialState, inner, eventHandler, persistState -> coVerifySequence { inner.beginPairingFlow("http:://example.com/pairing", listOf("scope1"), "test-entrypoint", any()) persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(connecting = false), - transition = FxaAuthStateTransition.OAUTH_FAILED_TO_BEGIN, + FxaEvent.AuthEvent( + FxaAuthEventKind.OAUTH_FAILED_TO_BEGIN, + initialState, ), ) } - FxaAuthState.Disconnected(connecting = false) + initialState }, - whenConnected = NoEffects, - whenConnectedIfThrows = NeverHappens, ) } @Test fun `FxaActionProcessor handles CompleteOauthFlow`() = runTest { - Mocks.verifyAction( + Mocks.verifyAll( completeAuthFlowAction, - initialStateDisconnected = FxaAuthState.Disconnected(connecting = true), - whenDisconnected = { inner, eventHandler, persistState -> + listOf(FxaAuthState.DISCONNECTED, FxaAuthState.AUTHENTICATING), + { _, inner, eventHandler, persistState -> coVerifySequence { inner.completeOauthFlow("test-code", "test-state") persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.OAUTH_COMPLETE, + FxaEvent.AuthEvent( + FxaAuthEventKind.OAUTH_COMPLETE, + FxaAuthState.CONNECTED, ), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, - whenDisconnectedIfThrows = { inner, eventHandler, persistState -> + { initialState, inner, eventHandler, persistState -> coVerifySequence { inner.completeOauthFlow("test-code", "test-state") persistState() @@ -267,409 +262,414 @@ class FxaActionProcessorTest { // progress. In order to unset it, the application needs to send // CancelOAuthFlow eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(connecting = true), - transition = FxaAuthStateTransition.OAUTH_FAILED_TO_COMPLETE, + FxaEvent.AuthEvent( + FxaAuthEventKind.OAUTH_FAILED_TO_COMPLETE, + initialState, ), ) } - FxaAuthState.Disconnected(connecting = true) + initialState }, - whenConnected = NoEffects, - whenConnectedIfThrows = NeverHappens, ) } @Test fun `FxaActionProcessor handles a failed oauth complete, then a successful one`() = runTest { - val mocks = Mocks.create(FxaAuthState.Disconnected(connecting = true)) + val mocks = Mocks.create(FxaAuthState.AUTHENTICATING) every { mocks.firefoxAccount.completeOauthFlow(any(), any()) } throws oauthStateException - - mocks.verifyAction(completeAuthFlowInvalidAction) { inner, eventHandler, persistState -> + mocks.verifyAction(completeAuthFlowInvalidAction) { _, inner, eventHandler, persistState -> coVerifySequence { inner.completeOauthFlow("test-code", "bad-state") persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(connecting = true), - transition = FxaAuthStateTransition.OAUTH_FAILED_TO_COMPLETE, + FxaEvent.AuthEvent( + FxaAuthEventKind.OAUTH_FAILED_TO_COMPLETE, + FxaAuthState.AUTHENTICATING, ), ) } - FxaAuthState.Disconnected(connecting = true) + FxaAuthState.AUTHENTICATING } every { mocks.firefoxAccount.completeOauthFlow(any(), any()) } just runs - mocks.verifyAction(completeAuthFlowAction) { inner, eventHandler, persistState -> + mocks.verifyAction(completeAuthFlowAction) { _, inner, eventHandler, persistState -> coVerifySequence { inner.completeOauthFlow("test-code", "test-state") persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.OAUTH_COMPLETE, + FxaEvent.AuthEvent( + FxaAuthEventKind.OAUTH_COMPLETE, + FxaAuthState.CONNECTED, ), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } } @Test fun `FxaActionProcessor handles CancelOauthFlow`() = runTest { - Mocks.verifyAction( + Mocks.verifyAll( FxaAction.CancelOAuthFlow, - initialStateDisconnected = FxaAuthState.Disconnected(connecting = true), - whenDisconnected = { _, eventHandler, _ -> + listOf(FxaAuthState.DISCONNECTED, FxaAuthState.AUTHENTICATING), + { _, _, eventHandler, _ -> coVerifySequence { eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(connecting = false), - transition = FxaAuthStateTransition.OAUTH_CANCELLED, + FxaEvent.AuthEvent( + FxaAuthEventKind.OAUTH_CANCELLED, + FxaAuthState.DISCONNECTED, ), ) } - FxaAuthState.Disconnected(connecting = false) + FxaAuthState.DISCONNECTED }, - whenDisconnectedIfThrows = NeverHappens, - whenConnected = NoEffects, - whenConnectedIfThrows = NeverHappens, + NeverThrows, ) } @Test fun `FxaActionProcessor handles Disconnect`() = runTest { - Mocks.verifyAction( - FxaAction.Disconnect(), - whenDisconnected = NoEffects, - whenDisconnectedIfThrows = NeverHappens, - whenConnected = { inner, eventHandler, persistState -> + Mocks.verifyAll( + FxaAction.Disconnect, + listOf(FxaAuthState.AUTHENTICATING, FxaAuthState.CONNECTED, FxaAuthState.CHECKING_AUTH, FxaAuthState.AUTH_ISSUES), + { _, inner, eventHandler, persistState -> coVerifySequence { inner.disconnect() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(), - transition = FxaAuthStateTransition.DISCONNECTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.DISCONNECTED, + FxaAuthState.DISCONNECTED, ), ) } - FxaAuthState.Disconnected() + FxaAuthState.DISCONNECTED }, - whenConnectedIfThrows = NeverHappens, + NeverThrows, ) } @Test - fun `FxaActionProcessor handles Disconnect(fromAuthIssues=true)`() = runTest { - Mocks.verifyAction( - FxaAction.Disconnect(fromAuthIssues = true), - whenDisconnected = NoEffects, - whenDisconnectedIfThrows = NeverHappens, - whenConnected = { inner, eventHandler, persistState -> + fun `FxaActionProcessor handles LogoutFromAuthIssues`() = runTest { + Mocks.verifyAll( + FxaAction.LogoutFromAuthIssues, + // Note: DISCONNECTED is not listed below. If the client is already disconnected, then + // it doesn't make sense to handle logoutFromAuthIssues() and transition them to the + // AUTH_ISSUES state. + listOf(FxaAuthState.AUTHENTICATING, FxaAuthState.CONNECTED, FxaAuthState.CHECKING_AUTH), + { _, inner, eventHandler, persistState -> coVerifySequence { - inner.disconnectFromAuthIssues() + inner.logoutFromAuthIssues() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(fromAuthIssues = true), - transition = FxaAuthStateTransition.AUTH_CHECK_FAILED, + FxaEvent.AuthEvent( + FxaAuthEventKind.LOGOUT_FROM_AUTH_ISSUES, + FxaAuthState.AUTH_ISSUES, ), ) } - FxaAuthState.Disconnected(fromAuthIssues = true) + FxaAuthState.AUTH_ISSUES }, - whenConnectedIfThrows = NeverHappens, + NeverThrows, ) } @Test fun `FxaActionProcessor handles CheckAuthorization`() = runTest { - Mocks.verifyAction( + Mocks.verifyAll( FxaAction.CheckAuthorization, - whenDisconnected = NoEffects, - whenDisconnectedIfThrows = NeverHappens, - whenConnected = { inner, eventHandler, persistState -> + listOf(FxaAuthState.CONNECTED), + { _, inner, eventHandler, persistState -> coVerifySequence { eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) inner.checkAuthorizationStatus() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.AUTH_CHECK_SUCCESS, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_SUCCESS, + FxaAuthState.CONNECTED, ), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, // If the auth check throws FxaException.Other, then then we currently consider that a // success, rather than kicking the user out of the account. Other failure models are // tested in the test cases below - whenConnectedIfThrows = { inner, eventHandler, persistState -> + { _, inner, eventHandler, persistState -> coVerifySequence { eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) inner.checkAuthorizationStatus() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.AUTH_CHECK_SUCCESS, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_SUCCESS, + FxaAuthState.CONNECTED, ), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, ) } @Test fun `FxaActionProcessor disconnects if checkAuthorizationStatus returns active=false`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) + val mocks = Mocks.create(FxaAuthState.CONNECTED) every { mocks.firefoxAccount.checkAuthorizationStatus() } returns AuthorizationInfo(active = false) - mocks.verifyAction(FxaAction.CheckAuthorization) { inner, eventHandler, persistState -> + + mocks.verifyAction(FxaAction.CheckAuthorization) { + _, inner, eventHandler, persistState -> coVerifySequence { eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) inner.checkAuthorizationStatus() - inner.disconnectFromAuthIssues() + inner.logoutFromAuthIssues() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(fromAuthIssues = true), - transition = FxaAuthStateTransition.AUTH_CHECK_FAILED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_FAILED, + FxaAuthState.AUTH_ISSUES, ), ) } - FxaAuthState.Disconnected(fromAuthIssues = true) + FxaAuthState.AUTH_ISSUES } } @Test fun `FxaActionProcessor disconnects if checkAuthorizationStatus throwns an auth exception`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) + val mocks = Mocks.create(FxaAuthState.CONNECTED) every { mocks.firefoxAccount.checkAuthorizationStatus() } throws authException - mocks.verifyAction(FxaAction.CheckAuthorization) { inner, eventHandler, persistState -> + mocks.verifyAction(FxaAction.CheckAuthorization) { + _, inner, eventHandler, persistState -> coVerifySequence { eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) inner.checkAuthorizationStatus() - inner.disconnectFromAuthIssues() + inner.logoutFromAuthIssues() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(fromAuthIssues = true), - transition = FxaAuthStateTransition.AUTH_CHECK_FAILED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_FAILED, + FxaAuthState.AUTH_ISSUES, ), ) } - FxaAuthState.Disconnected(fromAuthIssues = true) + FxaAuthState.AUTH_ISSUES } } @Test fun `FxaActionProcessor handles InitializeDevice`() = runTest { - Mocks.verifyAction( + Mocks.verifyAll( initializeDeviceAction, - whenDisconnected = NoEffects, - whenDisconnectedIfThrows = NeverHappens, - whenConnected = { inner, eventHandler, _ -> + listOf(FxaAuthState.CONNECTED), + { _, inner, eventHandler, _ -> coVerifySequence { inner.initializeDevice("My Phone", DeviceType.MOBILE, listOf(DeviceCapability.SEND_TAB)) eventHandler.onFxaEvent(FxaEvent.DeviceOperationComplete(FxaDeviceOperation.INITIALIZE_DEVICE, testLocalDevice)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, - whenConnectedIfThrows = { inner, eventHandler, _ -> + { _, inner, eventHandler, _ -> coVerifySequence { inner.initializeDevice("My Phone", DeviceType.MOBILE, listOf(DeviceCapability.SEND_TAB)) eventHandler.onFxaEvent(FxaEvent.DeviceOperationFailed(FxaDeviceOperation.INITIALIZE_DEVICE)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, ) } @Test fun `FxaActionProcessor handles EnsureCapabilities`() = runTest { - Mocks.verifyAction( + Mocks.verifyAll( ensureCapabilitiesAction, - whenDisconnected = NoEffects, - whenDisconnectedIfThrows = NeverHappens, - whenConnected = { inner, eventHandler, _ -> + listOf(FxaAuthState.CONNECTED), + { _, inner, eventHandler, _ -> coVerifySequence { inner.ensureCapabilities(listOf(DeviceCapability.SEND_TAB)) eventHandler.onFxaEvent( FxaEvent.DeviceOperationComplete(FxaDeviceOperation.ENSURE_CAPABILITIES, testLocalDevice), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, - whenConnectedIfThrows = { inner, eventHandler, _ -> + { _, inner, eventHandler, _ -> coVerifySequence { inner.ensureCapabilities(listOf(DeviceCapability.SEND_TAB)) eventHandler.onFxaEvent( FxaEvent.DeviceOperationFailed(FxaDeviceOperation.ENSURE_CAPABILITIES), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, ) } @Test fun `FxaActionProcessor handles SetDeviceName`() = runTest { - Mocks.verifyAction( + Mocks.verifyAll( setDeviceNameAction, - whenDisconnected = NoEffects, - whenDisconnectedIfThrows = NeverHappens, - whenConnected = { inner, eventHandler, _ -> + listOf(FxaAuthState.CONNECTED), + { _, inner, eventHandler, _ -> coVerifySequence { inner.setDeviceName("My Phone") eventHandler.onFxaEvent( FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, - whenConnectedIfThrows = { inner, eventHandler, _ -> + { _, inner, eventHandler, _ -> coVerifySequence { inner.setDeviceName("My Phone") eventHandler.onFxaEvent( FxaEvent.DeviceOperationFailed(FxaDeviceOperation.SET_DEVICE_NAME), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, ) } @Test fun `FxaActionProcessor handles SetDevicePushSubscription`() = runTest { - Mocks.verifyAction( + Mocks.verifyAll( setDevicePushSubscriptionAction, - whenDisconnected = NoEffects, - whenDisconnectedIfThrows = NeverHappens, - whenConnected = { inner, eventHandler, _ -> + listOf(FxaAuthState.CONNECTED), + { _, inner, eventHandler, _ -> coVerifySequence { inner.setPushSubscription( DevicePushSubscription("endpoint", "public-key", "auth-key"), ) eventHandler.onFxaEvent(FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_PUSH_SUBSCRIPTION, testLocalDevice)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, - whenConnectedIfThrows = { inner, eventHandler, _ -> + { _, inner, eventHandler, _ -> coVerifySequence { inner.setPushSubscription( DevicePushSubscription("endpoint", "public-key", "auth-key"), ) eventHandler.onFxaEvent(FxaEvent.DeviceOperationFailed(FxaDeviceOperation.SET_DEVICE_PUSH_SUBSCRIPTION)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, ) } @Test fun `FxaActionProcessor handles SendSingleTab`() = runTest { - Mocks.verifyAction( + Mocks.verifyAll( sendSingleTabAction, - whenDisconnected = NoEffects, - whenDisconnectedIfThrows = NeverHappens, - whenConnected = { inner, _, _ -> + listOf(FxaAuthState.CONNECTED), + { _, inner, _, _ -> coVerifySequence { inner.sendSingleTab("my-other-device", "My page", "http://example.com/sent-tab") } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, - whenConnectedIfThrows = { inner, _, _ -> + { _, inner, _, _ -> coVerifySequence { inner.sendSingleTab("my-other-device", "My page", "http://example.com/sent-tab") // Should we notify clients if this fails? There doesn't seem like there's much // they can do about it. } - FxaAuthState.Connected() + FxaAuthState.CONNECTED }, ) } @Test fun `FxaActionProcessor sends OAuth results to the deferred`() = runTest { - val mocks = Mocks.create(FxaAuthState.Disconnected()) + val firefoxAccount = mockFirefoxAccount() + val actionProcessor = FxaActionProcessor( + firefoxAccount, + mockEventHandler(), + mockPersistState(), + FxaAuthState.DISCONNECTED, + ) CompletableDeferred().let { - mocks.actionProcessor.processAction(beginOAuthFlowAction.copy(result = it)) + actionProcessor.processAction(beginOAuthFlowAction.copy(result = it)) assertEquals(it.await(), "http://example.com/oauth-flow-start") } CompletableDeferred().let { - mocks.actionProcessor.processAction(beginPairingFlowAction.copy(result = it)) + actionProcessor.processAction(beginPairingFlowAction.copy(result = it)) assertEquals(it.await(), "http://example.com/pairing-flow-start") } - every { mocks.firefoxAccount.beginOauthFlow(any(), any(), any()) } throws testException - every { mocks.firefoxAccount.beginPairingFlow(any(), any(), any(), any()) } throws testException + every { firefoxAccount.beginOauthFlow(any(), any(), any()) } throws testException + every { firefoxAccount.beginPairingFlow(any(), any(), any(), any()) } throws testException CompletableDeferred().let { - mocks.actionProcessor.processAction(beginOAuthFlowAction.copy(result = it)) + actionProcessor.processAction(beginOAuthFlowAction.copy(result = it)) assertEquals(it.await(), null) } CompletableDeferred().let { - mocks.actionProcessor.processAction(beginPairingFlowAction.copy(result = it)) + actionProcessor.processAction(beginPairingFlowAction.copy(result = it)) assertEquals(it.await(), null) } } @Test fun `FxaActionProcessor sends Device operation results to the deferred`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) + val firefoxAccount = mockFirefoxAccount() + val actionProcessor = FxaActionProcessor( + firefoxAccount, + mockEventHandler(), + mockPersistState(), + FxaAuthState.CONNECTED, + ) CompletableDeferred().let { - mocks.actionProcessor.processAction(initializeDeviceAction.copy(result = it)) + actionProcessor.processAction(initializeDeviceAction.copy(result = it)) assertEquals(it.await(), true) } CompletableDeferred().let { - mocks.actionProcessor.processAction(ensureCapabilitiesAction.copy(result = it)) + actionProcessor.processAction(ensureCapabilitiesAction.copy(result = it)) assertEquals(it.await(), true) } CompletableDeferred().let { - mocks.actionProcessor.processAction(setDeviceNameAction.copy(result = it)) + actionProcessor.processAction(setDeviceNameAction.copy(result = it)) assertEquals(it.await(), true) } CompletableDeferred().let { - mocks.actionProcessor.processAction(setDevicePushSubscriptionAction.copy(result = it)) + actionProcessor.processAction(setDevicePushSubscriptionAction.copy(result = it)) assertEquals(it.await(), true) } CompletableDeferred().let { - mocks.actionProcessor.processAction(sendSingleTabAction.copy(result = it)) + actionProcessor.processAction(sendSingleTabAction.copy(result = it)) assertEquals(it.await(), true) } - mocks.firefoxAccount.apply { + firefoxAccount.apply { every { initializeDevice(any(), any(), any()) } throws testException every { ensureCapabilities(any()) } throws testException every { setDeviceName(any()) } throws testException @@ -678,27 +678,27 @@ class FxaActionProcessorTest { } CompletableDeferred().let { - mocks.actionProcessor.processAction(initializeDeviceAction.copy(result = it)) + actionProcessor.processAction(initializeDeviceAction.copy(result = it)) assertEquals(it.await(), false) } CompletableDeferred().let { - mocks.actionProcessor.processAction(ensureCapabilitiesAction.copy(result = it)) + actionProcessor.processAction(ensureCapabilitiesAction.copy(result = it)) assertEquals(it.await(), false) } CompletableDeferred().let { - mocks.actionProcessor.processAction(setDeviceNameAction.copy(result = it)) + actionProcessor.processAction(setDeviceNameAction.copy(result = it)) assertEquals(it.await(), false) } CompletableDeferred().let { - mocks.actionProcessor.processAction(setDevicePushSubscriptionAction.copy(result = it)) + actionProcessor.processAction(setDevicePushSubscriptionAction.copy(result = it)) assertEquals(it.await(), false) } CompletableDeferred().let { - mocks.actionProcessor.processAction(sendSingleTabAction.copy(result = it)) + actionProcessor.processAction(sendSingleTabAction.copy(result = it)) assertEquals(it.await(), false) } } @@ -710,10 +710,10 @@ class FxaActionProcessorTest { throw testException } } - FxaActionProcessor(mockk(), eventHandler, mockk(), initialState = FxaAuthState.Connected()).sendEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(fromAuthIssues = true), - transition = FxaAuthStateTransition.AUTH_CHECK_FAILED, + FxaActionProcessor(mockk(), eventHandler, mockk(), initialState = FxaAuthState.CONNECTED).sendEvent( + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_FAILED, + FxaAuthState.AUTH_ISSUES, ), ) // Check that the handler was called and threw an exception, but sendEvent caught it @@ -748,12 +748,10 @@ class FxaActionProcessorTest { class FxaRetryTest { @Test fun `FxaActionProcessor retries after network errors`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) - every { - mocks.firefoxAccount.setDeviceName(any()) - } throws networkException andThen testLocalDevice + val mocks = Mocks.create(FxaAuthState.CONNECTED) + every { mocks.firefoxAccount.setDeviceName(any()) } throws networkException andThen testLocalDevice - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, _ -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, _ -> coVerifySequence { // This throws FxaException.Network, we should retry inner.setDeviceName("My Phone") @@ -763,16 +761,16 @@ class FxaRetryTest { FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } } @Test fun `FxaActionProcessor fails after 2 network errors`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) + val mocks = Mocks.create(FxaAuthState.CONNECTED) every { mocks.firefoxAccount.setDeviceName(any()) } throws networkException - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, _ -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, _ -> coVerifySequence { // This throws FxaException.Network, we should retry inner.setDeviceName("My Phone") @@ -780,18 +778,16 @@ class FxaRetryTest { inner.setDeviceName("My Phone") eventHandler.onFxaEvent(FxaEvent.DeviceOperationFailed(FxaDeviceOperation.SET_DEVICE_NAME)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } } @Test fun `FxaActionProcessor fails after multiple network errors in a short time`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) - every { - mocks.firefoxAccount.setDeviceName(any()) - } throws networkException andThen testLocalDevice + val mocks = Mocks.create(FxaAuthState.CONNECTED) + every { mocks.firefoxAccount.setDeviceName(any()) } throws networkException andThen testLocalDevice - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, _ -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, _ -> coVerifySequence { // This fails with FxaException.Network, we should retry inner.setDeviceName("My Phone") @@ -801,7 +797,7 @@ class FxaRetryTest { FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } mocks.actionProcessor.retryLogic.fastForward(29.seconds) @@ -809,7 +805,7 @@ class FxaRetryTest { mocks.firefoxAccount.setDeviceName(any()) } throws networkException andThen testLocalDevice - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, _ -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, _ -> coVerifySequence { // This throws again and the timeout period is still active, we should fail inner.setDeviceName("My Phone") @@ -817,18 +813,16 @@ class FxaRetryTest { FxaEvent.DeviceOperationFailed(FxaDeviceOperation.SET_DEVICE_NAME), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } } @Test fun `FxaActionProcessor retrys network errors again after a timeout period`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) - every { - mocks.firefoxAccount.setDeviceName(any()) - } throws networkException andThen testLocalDevice + val mocks = Mocks.create(FxaAuthState.CONNECTED) + every { mocks.firefoxAccount.setDeviceName(any()) } throws networkException andThen testLocalDevice - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, _ -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, _ -> coVerifySequence { // This fails with FxaException.Network, we should retry inner.setDeviceName("My Phone") @@ -838,15 +832,13 @@ class FxaRetryTest { FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } mocks.actionProcessor.retryLogic.fastForward(31.seconds) - every { - mocks.firefoxAccount.setDeviceName(any()) - } throws networkException andThen testLocalDevice + every { mocks.firefoxAccount.setDeviceName(any()) } throws networkException andThen testLocalDevice - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, _ -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, _ -> coVerifySequence { // Timeout period over, we should retry this time inner.setDeviceName("My Phone") @@ -856,154 +848,145 @@ class FxaRetryTest { FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice), ) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } } @Test fun `FxaActionProcessor calls checkAuthorizationStatus after auth errors`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) - every { - mocks.firefoxAccount.setDeviceName(any()) - } throws authException andThen testLocalDevice + val mocks = Mocks.create(FxaAuthState.CONNECTED) + every { mocks.firefoxAccount.setDeviceName(any()) } throws authException andThen testLocalDevice - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, persistState -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, persistState -> coVerifySequence { // This throws FxaException.Authentication, we should recheck the auth status inner.setDeviceName("My Phone") // The auth check works eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) inner.checkAuthorizationStatus() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.AUTH_CHECK_SUCCESS, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_SUCCESS, + FxaAuthState.CONNECTED, ), ) // .. continue on inner.setDeviceName("My Phone") eventHandler.onFxaEvent(FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } } @Test fun `FxaActionProcessor fails after 2 auth errors`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) - every { - mocks.firefoxAccount.setDeviceName(any()) - } throws authException + val mocks = Mocks.create(FxaAuthState.CONNECTED) + every { mocks.firefoxAccount.setDeviceName(any()) } throws authException - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, persistState -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, persistState -> coVerifySequence { // This throws FxaException.Authentication, we should recheck the auth status inner.setDeviceName("My Phone") // The auth check works eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) inner.checkAuthorizationStatus() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.AUTH_CHECK_SUCCESS, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_SUCCESS, + FxaAuthState.CONNECTED, ), ) // .. but this throws again, inner.setDeviceName("My Phone") // ..so save the state, transition to disconnected, and make the operation fail - inner.disconnectFromAuthIssues() + inner.logoutFromAuthIssues() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(fromAuthIssues = true), - transition = FxaAuthStateTransition.AUTH_CHECK_FAILED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_FAILED, + FxaAuthState.AUTH_ISSUES, ), ) eventHandler.onFxaEvent(FxaEvent.DeviceOperationFailed(FxaDeviceOperation.SET_DEVICE_NAME)) } - FxaAuthState.Disconnected(fromAuthIssues = true) + FxaAuthState.AUTH_ISSUES } } @Test fun `FxaActionProcessor fails if the auth check fails`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) - every { - mocks.firefoxAccount.setDeviceName(any()) - } throws authException andThen testLocalDevice - + val mocks = Mocks.create(FxaAuthState.CONNECTED) + every { mocks.firefoxAccount.setDeviceName(any()) } throws authException andThen testLocalDevice every { mocks.firefoxAccount.checkAuthorizationStatus() } returns AuthorizationInfo(active = false) - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, persistState -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, persistState -> coVerify { // This throws FxaException.Authentication, we should recheck the auth status inner.setDeviceName("My Phone") // The auth check fails eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) inner.checkAuthorizationStatus() - inner.disconnectFromAuthIssues() + inner.logoutFromAuthIssues() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(fromAuthIssues = true), - transition = FxaAuthStateTransition.AUTH_CHECK_FAILED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_FAILED, + FxaAuthState.AUTH_ISSUES, ), ) // .. so the operation fails eventHandler.onFxaEvent(FxaEvent.DeviceOperationFailed(FxaDeviceOperation.SET_DEVICE_NAME)) } - FxaAuthState.Disconnected(fromAuthIssues = true) + FxaAuthState.AUTH_ISSUES } } @Test fun `FxaActionProcessor fails after multiple auth errors in a short time`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) - every { - mocks.firefoxAccount.setDeviceName(any()) - } throws authException andThen testLocalDevice + val mocks = Mocks.create(FxaAuthState.CONNECTED) + every { mocks.firefoxAccount.setDeviceName(any()) } throws authException andThen testLocalDevice - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, persistState -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, persistState -> coVerify { // This throws FxaException.Authentication, we should recheck the auth status inner.setDeviceName("My Phone") // The auth check works eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) inner.checkAuthorizationStatus() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.AUTH_CHECK_SUCCESS, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_SUCCESS, + FxaAuthState.CONNECTED, ), ) // .. continue on inner.setDeviceName("My Phone") eventHandler.onFxaEvent(FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } mocks.actionProcessor.retryLogic.fastForward(59.seconds) @@ -1011,56 +994,56 @@ class FxaRetryTest { mocks.firefoxAccount.setDeviceName(any()) } throws authException andThen testLocalDevice - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, persistState -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, persistState -> coVerify { // This throws, inner.setDeviceName("My Phone") // ..so save the state, transition to disconnected, and make the operation fail - inner.disconnectFromAuthIssues() + inner.logoutFromAuthIssues() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Disconnected(fromAuthIssues = true), - transition = FxaAuthStateTransition.AUTH_CHECK_FAILED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_FAILED, + FxaAuthState.AUTH_ISSUES, ), ) eventHandler.onFxaEvent(FxaEvent.DeviceOperationFailed(FxaDeviceOperation.SET_DEVICE_NAME)) } - FxaAuthState.Disconnected(fromAuthIssues = true) + FxaAuthState.AUTH_ISSUES } } @Test fun `FxaActionProcessor checks authorization again after timeout period passes`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) + val mocks = Mocks.create(FxaAuthState.CONNECTED) every { mocks.firefoxAccount.setDeviceName(any()) } throws authException andThen testLocalDevice - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, persistState -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, persistState -> coVerify { // This throws FxaException.Authentication, we should recheck the auth status inner.setDeviceName("My Phone") // The auth check works eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) inner.checkAuthorizationStatus() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.AUTH_CHECK_SUCCESS, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_SUCCESS, + FxaAuthState.CONNECTED, ), ) // .. continue on inner.setDeviceName("My Phone") eventHandler.onFxaEvent(FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } mocks.actionProcessor.retryLogic.fastForward(61.seconds) @@ -1068,36 +1051,36 @@ class FxaRetryTest { mocks.firefoxAccount.setDeviceName(any()) } throws authException andThen testLocalDevice - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, persistState -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, persistState -> coVerify { // Timeout period over, we should recheck the auth status this time inner.setDeviceName("My Phone") // The auth check works eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) inner.checkAuthorizationStatus() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.AUTH_CHECK_SUCCESS, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_SUCCESS, + FxaAuthState.CONNECTED, ), ) // .. continue on inner.setDeviceName("My Phone") eventHandler.onFxaEvent(FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } } @Test fun `FxaActionProcessor retries after an auth + network exception`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) + val mocks = Mocks.create(FxaAuthState.CONNECTED) every { mocks.firefoxAccount.setDeviceName(any()) } throws authException andThen testLocalDevice @@ -1106,14 +1089,14 @@ class FxaRetryTest { mocks.firefoxAccount.checkAuthorizationStatus() } throws networkException andThen AuthorizationInfo(active = true) - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, persistState -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, persistState -> coVerify { // This throws FxaException.Authentication, we should recheck the auth status inner.setDeviceName("My Phone") eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) // This throws a network error, we should retry @@ -1122,22 +1105,22 @@ class FxaRetryTest { inner.checkAuthorizationStatus() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.AUTH_CHECK_SUCCESS, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_SUCCESS, + FxaAuthState.CONNECTED, ), ) // .. continue on inner.setDeviceName("My Phone") eventHandler.onFxaEvent(FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } } @Test fun `FxaActionProcessor retries after a network + auth exception`() = runTest { - val mocks = Mocks.create(FxaAuthState.Connected()) + val mocks = Mocks.create(FxaAuthState.CONNECTED) every { mocks.firefoxAccount.setDeviceName(any()) } throws authException andThen testLocalDevice @@ -1146,32 +1129,32 @@ class FxaRetryTest { mocks.firefoxAccount.checkAuthorizationStatus() } throws networkException andThen AuthorizationInfo(active = true) - mocks.verifyAction(setDeviceNameAction) { inner, eventHandler, persistState -> + mocks.verifyAction(setDeviceNameAction) { _, inner, eventHandler, persistState -> coVerify { // This throws FxaException.Network should try again inner.setDeviceName("My Phone") // This throws FxaException.Authentication, we should recheck the auth status inner.setDeviceName("My Phone") eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(authCheckInProgress = true), - transition = FxaAuthStateTransition.AUTH_CHECK_STARTED, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_STARTED, + FxaAuthState.CHECKING_AUTH, ), ) // This works inner.checkAuthorizationStatus() persistState() eventHandler.onFxaEvent( - FxaEvent.AuthStateChanged( - newState = FxaAuthState.Connected(), - transition = FxaAuthStateTransition.AUTH_CHECK_SUCCESS, + FxaEvent.AuthEvent( + FxaAuthEventKind.AUTH_CHECK_SUCCESS, + FxaAuthState.CONNECTED, ), ) // .. continue on inner.setDeviceName("My Phone") eventHandler.onFxaEvent(FxaEvent.DeviceOperationComplete(FxaDeviceOperation.SET_DEVICE_NAME, testLocalDevice)) } - FxaAuthState.Connected() + FxaAuthState.CONNECTED } } } diff --git a/components/fxa-client/src/auth.rs b/components/fxa-client/src/auth.rs index 6c68461cc6..5255a178a1 100644 --- a/components/fxa-client/src/auth.rs +++ b/components/fxa-client/src/auth.rs @@ -161,17 +161,18 @@ impl FirefoxAccount { /// the user to reconnect to their account. If reconnecting to the same account /// is not desired then the application should discard the persisted account state. pub fn disconnect(&self) { - self.internal.lock().disconnect(false) + self.internal.lock().disconnect() } - /// Disconnect because of auth issues. + /// Log out because of authorization / authentication issues + /// /// /// **💾 This method alters the persisted account state.** /// - /// This works exactly like `disconnect`, except the - /// `AuthState::Disconnected::from_auth_issues` flag will be set when get_auth_state() is called. - pub fn disconnect_from_auth_issues(&self) { - self.internal.lock().disconnect(true) + /// Call this if you know there's an authentication / authorization issue that requires the + /// user to re-authenticated. It transitions the user to the [FxaRustAuthState.AuthIssues] state. + pub fn logout_from_auth_issues(&self) { + self.internal.lock().logout_from_auth_issues() } /// Used by the application to test auth token issues @@ -200,14 +201,15 @@ pub struct MetricsParams { /// High-level view of the authorization state /// -/// This is named `FxaRustAuthState` because it doesn't track everything we want yet and needs help -/// from the wrapper code. The wrapper code defines the actual `FxaAuthState` type based on this, -/// adding the extra data. +/// This is named `FxaRustAuthState` because it doesn't track all the states we want yet and needs +/// help from the wrapper code. The wrapper code defines the actual `FxaAuthState` type based on +/// this, adding the extra data. /// /// In the long-term, we should track that data in Rust, remove the wrapper, and rename this to /// `FxaAuthState`. #[derive(Debug, PartialEq, Eq)] pub enum FxaRustAuthState { - Disconnected { from_auth_issues: bool }, + Disconnected, Connected, + AuthIssues, } diff --git a/components/fxa-client/src/fxa_client.udl b/components/fxa-client/src/fxa_client.udl index ddc49a8bf1..5d17d23796 100644 --- a/components/fxa-client/src/fxa_client.udl +++ b/components/fxa-client/src/fxa_client.udl @@ -247,13 +247,14 @@ interface FirefoxAccount { // void disconnect(); - // Disconnect because of auth issues. + // Log out because of authorization / authentication issues + // // // **💾 This method alters the persisted account state.** // - // This works exactly like `disconnect`, except the - // `AuthState::Disconnected::from_auth_issues` flag will be set when get_auth_state() is called. - void disconnect_from_auth_issues(); + // Call this if you know there's an authentication / authorization issue that requires the + // user to re-authenticated. It transitions the user to the [FxaRustAuthState.AuthIssues] state. + void logout_from_auth_issues(); // Get the high-level authentication state of the client FxaRustAuthState get_auth_state(); @@ -924,10 +925,10 @@ dictionary Profile { boolean is_default_avatar; }; -[Enum] -interface FxaRustAuthState { - Disconnected(boolean from_auth_issues); - Connected(); +enum FxaRustAuthState { + "Disconnected", + "Connected", + "AuthIssues", }; // A "capability" offered by a device. diff --git a/components/fxa-client/src/internal/mod.rs b/components/fxa-client/src/internal/mod.rs index 3ba97b55d7..9c65ad6cfa 100644 --- a/components/fxa-client/src/internal/mod.rs +++ b/components/fxa-client/src/internal/mod.rs @@ -78,7 +78,7 @@ impl FirefoxAccount { current_device_id: None, last_seen_profile: None, access_token_cache: HashMap::new(), - disconnected_from_auth_issues: false, + logged_out_from_auth_issues: false, }) } @@ -196,7 +196,7 @@ impl FirefoxAccount { /// the device could still be in the FxA devices manager. /// /// **💾 This method alters the persisted account state.** - pub fn disconnect(&mut self, from_auth_issues: bool) { + pub fn disconnect(&mut self) { let current_device_result; { current_device_result = self.get_current_device(); @@ -221,7 +221,20 @@ impl FirefoxAccount { log::warn!("Error while destroying the device: {}", e); } } - self.state.disconnect(from_auth_issues); + self.state.disconnect(); + self.clear_devices_and_attached_clients_cache(); + self.telemetry = FxaTelemetry::new(); + } + + /// Log out because of authorization / authentication issues + /// + /// + /// **💾 This method alters the persisted account state.** + /// + /// Call this if you know there's an authentication / authorization issue that requires the + /// user to re-authenticated. It transitions the user to the [FxaRustAuthState.AuthIssues] state. + pub fn logout_from_auth_issues(&mut self) { + self.state.logout_from_auth_issues(); self.clear_devices_and_attached_clients_cache(); self.telemetry = FxaTelemetry::new(); } @@ -353,7 +366,7 @@ mod tests { fxa.set_client(Arc::new(client)); assert!(!fxa.state.is_access_token_cache_empty()); - fxa.disconnect(false); + fxa.disconnect(); assert!(fxa.state.is_access_token_cache_empty()); } @@ -422,7 +435,7 @@ mod tests { fxa.set_client(Arc::new(client)); assert!(fxa.state.refresh_token().is_some()); - fxa.disconnect(false); + fxa.disconnect(); assert!(fxa.state.refresh_token().is_none()); } @@ -469,7 +482,7 @@ mod tests { fxa.set_client(Arc::new(client)); assert!(fxa.state.refresh_token().is_some()); - fxa.disconnect(false); + fxa.disconnect(); assert!(fxa.state.refresh_token().is_none()); } @@ -505,7 +518,7 @@ mod tests { fxa.set_client(Arc::new(client)); assert!(fxa.state.refresh_token().is_some()); - fxa.disconnect(false); + fxa.disconnect(); assert!(fxa.state.refresh_token().is_none()); } @@ -524,9 +537,7 @@ mod tests { // The state starts as disconnected assert_auth_state( &fxa, - FxaRustAuthState::Disconnected { - from_auth_issues: false, - }, + FxaRustAuthState::Disconnected, ); // When we get the refresh tokens the state changes to connected @@ -536,20 +547,16 @@ mod tests { }); assert_auth_state(&fxa, FxaRustAuthState::Connected); - fxa.disconnect(true); + fxa.disconnect(); assert_auth_state( &fxa, - FxaRustAuthState::Disconnected { - from_auth_issues: true, - }, + FxaRustAuthState::Disconnected, ); - fxa.disconnect(false); + fxa.disconnect(); assert_auth_state( &fxa, - FxaRustAuthState::Disconnected { - from_auth_issues: false, - }, + FxaRustAuthState::Disconnected, ); } diff --git a/components/fxa-client/src/internal/push.rs b/components/fxa-client/src/internal/push.rs index 8e20e4112d..6632cb8be6 100644 --- a/components/fxa-client/src/internal/push.rs +++ b/components/fxa-client/src/internal/push.rs @@ -48,7 +48,7 @@ impl FirefoxAccount { }; if is_local_device { // Note: self.disconnect calls self.start_over which clears the state for the FirefoxAccount instance - self.disconnect(false); + self.disconnect(); } Ok(AccountEvent::DeviceDisconnected { device_id, diff --git a/components/fxa-client/src/internal/state_manager.rs b/components/fxa-client/src/internal/state_manager.rs index d128c16734..d6c91e87aa 100644 --- a/components/fxa-client/src/internal/state_manager.rs +++ b/components/fxa-client/src/internal/state_manager.rs @@ -161,23 +161,52 @@ impl StateManager { } self.persisted_state.refresh_token = Some(refresh_token); self.persisted_state.session_token = session_token; + self.persisted_state.logged_out_from_auth_issues = false; self.flow_store.clear(); } /// Called when the account is disconnected. This clears most of the auth state, but keeps /// some information in order to eventually reconnect to the same user account later. - pub fn disconnect(&mut self, from_auth_issues: bool) { - self.persisted_state = self.persisted_state.start_over(from_auth_issues); + pub fn disconnect(&mut self) { + // Clear (almost all of) the persisted state of the account. + // + // This method keep just enough information to be able to eventually reconnect + // to the same user account later. To completely forget the previously-signed-in + // user, simply discard the persisted data. + + self.persisted_state.current_device_id = None; + self.persisted_state.refresh_token = None; + self.persisted_state.scoped_keys = HashMap::new(); + self.persisted_state.last_handled_command = None; + self.persisted_state.commands_data = HashMap::new(); + self.persisted_state.access_token_cache = HashMap::new(); + self.persisted_state.server_local_device_info = None; + self.persisted_state.session_token = None; + self.persisted_state.logged_out_from_auth_issues = false; + self.flow_store.clear(); + } + + /// Called when the account is logged out because of auth issues. This clears some of the auth + /// state, but less than disconnect(). + pub fn logout_from_auth_issues(&mut self) { + self.persisted_state.refresh_token = None; + self.persisted_state.scoped_keys = HashMap::new(); + self.persisted_state.last_handled_command = None; + self.persisted_state.commands_data = HashMap::new(); + self.persisted_state.access_token_cache = HashMap::new(); + self.persisted_state.server_local_device_info = None; + self.persisted_state.session_token = None; + self.persisted_state.logged_out_from_auth_issues = true; self.flow_store.clear(); } pub fn get_auth_state(&self) -> FxaRustAuthState { if self.persisted_state.refresh_token.is_some() { FxaRustAuthState::Connected + } else if self.persisted_state.logged_out_from_auth_issues { + FxaRustAuthState::AuthIssues } else { - FxaRustAuthState::Disconnected { - from_auth_issues: self.persisted_state.disconnected_from_auth_issues, - } + FxaRustAuthState::Disconnected } } diff --git a/components/fxa-client/src/internal/state_persistence.rs b/components/fxa-client/src/internal/state_persistence.rs index 512f0720f6..6b0c5cbffe 100644 --- a/components/fxa-client/src/internal/state_persistence.rs +++ b/components/fxa-client/src/internal/state_persistence.rs @@ -83,8 +83,9 @@ enum PersistedStateTagged { /// If so then you'll need to tell serde how to fill in a suitable default. /// If not then you'll need to make a new `StateV3` and implement an explicit migration. /// -/// * Does the new field need to be modified when the user disconnects from the account? -/// If so then you'll need to update `StateV2.start_over` function. +/// * How does the new field need to be modified when the user disconnects from the account or is +/// logged out from auth issues? Update [state_manager.disconnect] and +/// [state_manager.logout_from_auth_issues]. /// #[derive(Clone, Serialize, Deserialize)] pub(crate) struct StateV2 { @@ -106,32 +107,7 @@ pub(crate) struct StateV2 { #[serde(default)] pub(crate) server_local_device_info: Option, #[serde(default)] - pub(crate) disconnected_from_auth_issues: bool, -} - -impl StateV2 { - /// Clear (almost all of) the persisted state of the account. - /// - /// This method keep just enough information to be able to eventually reconnect - /// to the same user account later. To completely forget the previously-signed-in - /// user, simply discard the persisted data. - /// - pub(crate) fn start_over(&self, disconnected_from_auth_issues: bool) -> StateV2 { - StateV2 { - config: self.config.clone(), - current_device_id: None, - // Leave the profile cache untouched so we can reconnect later. - last_seen_profile: self.last_seen_profile.clone(), - refresh_token: None, - scoped_keys: HashMap::new(), - last_handled_command: None, - commands_data: HashMap::new(), - access_token_cache: HashMap::new(), - server_local_device_info: None, - session_token: None, - disconnected_from_auth_issues, - } - } + pub(crate) logged_out_from_auth_issues: bool, } #[cfg(test)]