From fc782e48952d3df91768b9d02875cab663fe320c Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Tue, 1 Apr 2025 10:43:20 -0300 Subject: [PATCH] Move SignOut to usecase --- .../auth/cognito/AWSCognitoAuthPlugin.kt | 6 +- .../auth/cognito/KotlinAuthFacadeInternal.kt | 18 -- .../auth/cognito/RealAWSCognitoAuthPlugin.kt | 157 -------------- .../cognito/usecases/AuthUseCaseFactory.kt | 9 + .../ClearFederationToIdentityPoolUseCase.kt | 67 ++++++ .../auth/cognito/usecases/SignOutUseCase.kt | 132 ++++++++++++ .../auth/cognito/AWSCognitoAuthPluginTest.kt | 9 +- .../auth/cognito/AuthValidationTest.kt | 13 +- ...learFederationToIdentityPoolUseCaseTest.kt | 108 ++++++++++ .../cognito/usecases/SignOutUseCaseTest.kt | 201 ++++++++++++++++++ 10 files changed, 533 insertions(+), 187 deletions(-) create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCase.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCase.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCaseTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCaseTest.kt diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt index 0ec2f74e2..c43473562 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt @@ -404,12 +404,12 @@ class AWSCognitoAuthPlugin : AuthPlugin() { override fun signOut(onComplete: Consumer) = enqueue( onComplete, onError = ::throwIt - ) { queueFacade.signOut() } + ) { useCaseFactory.signOut().execute() } override fun signOut(options: AuthSignOutOptions, onComplete: Consumer) = enqueue( onComplete, onError = ::throwIt - ) { queueFacade.signOut(options) } + ) { useCaseFactory.signOut().execute(options) } override fun deleteUser(onSuccess: Action, onError: Consumer) = enqueue(onSuccess, onError) { useCaseFactory.deleteUser().execute() @@ -523,7 +523,7 @@ class AWSCognitoAuthPlugin : AuthPlugin() { * @param onError Error callback */ fun clearFederationToIdentityPool(onSuccess: Action, onError: Consumer) = - enqueue(onSuccess, onError) { queueFacade.clearFederationToIdentityPool() } + enqueue(onSuccess, onError) { useCaseFactory.clearFederationToIdentityPool().execute() } fun fetchMFAPreference(onSuccess: Consumer, onError: Consumer) = enqueue(onSuccess, onError) { useCaseFactory.fetchMfaPreference().execute() } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt index eaa0642d4..76e115651 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt @@ -22,10 +22,8 @@ import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult import com.amplifyframework.auth.options.AuthFetchSessionOptions -import com.amplifyframework.auth.options.AuthSignOutOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions import com.amplifyframework.auth.result.AuthSignInResult -import com.amplifyframework.auth.result.AuthSignOutResult import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -92,15 +90,6 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth { continuation.resumeWithException(it) } ) } - - suspend fun signOut(): AuthSignOutResult = suspendCoroutine { continuation -> - delegate.signOut { continuation.resume(it) } - } - - suspend fun signOut(options: AuthSignOutOptions): AuthSignOutResult = suspendCoroutine { continuation -> - delegate.signOut(options) { continuation.resume(it) } - } - suspend fun federateToIdentityPool( providerToken: String, authProvider: AuthProvider, @@ -114,11 +103,4 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth { continuation.resumeWithException(it) } ) } - - suspend fun clearFederationToIdentityPool() = suspendCoroutine { continuation -> - delegate.clearFederationToIdentityPool( - { continuation.resume(Unit) }, - { continuation.resumeWithException(it) } - ) - } } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt index e7a353efb..7ab3e8246 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt @@ -35,14 +35,9 @@ import com.amplifyframework.auth.cognito.exceptions.service.InvalidAccountTypeEx import com.amplifyframework.auth.cognito.exceptions.service.UserCancelledException import com.amplifyframework.auth.cognito.helpers.HostedUIHelper import com.amplifyframework.auth.cognito.helpers.identityProviderName -import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions import com.amplifyframework.auth.cognito.options.FederateToIdentityPoolOptions -import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult -import com.amplifyframework.auth.cognito.result.GlobalSignOutError -import com.amplifyframework.auth.cognito.result.HostedUIError -import com.amplifyframework.auth.cognito.result.RevokeTokenError import com.amplifyframework.auth.exceptions.ConfigurationException import com.amplifyframework.auth.exceptions.InvalidStateException import com.amplifyframework.auth.exceptions.NotAuthorizedException @@ -51,13 +46,10 @@ import com.amplifyframework.auth.exceptions.SessionExpiredException import com.amplifyframework.auth.exceptions.SignedOutException import com.amplifyframework.auth.exceptions.UnknownException import com.amplifyframework.auth.options.AuthFetchSessionOptions -import com.amplifyframework.auth.options.AuthSignOutOptions import com.amplifyframework.auth.options.AuthWebUISignInOptions import com.amplifyframework.auth.result.AuthSignInResult -import com.amplifyframework.auth.result.AuthSignOutResult import com.amplifyframework.auth.result.step.AuthNextSignInStep import com.amplifyframework.auth.result.step.AuthSignInStep -import com.amplifyframework.core.Action import com.amplifyframework.core.Amplify import com.amplifyframework.core.Consumer import com.amplifyframework.hub.HubChannel @@ -69,7 +61,6 @@ import com.amplifyframework.statemachine.codegen.data.FederatedToken import com.amplifyframework.statemachine.codegen.data.HostedUIErrorData import com.amplifyframework.statemachine.codegen.data.SignInData import com.amplifyframework.statemachine.codegen.data.SignInMethod -import com.amplifyframework.statemachine.codegen.data.SignOutData import com.amplifyframework.statemachine.codegen.errors.SessionError import com.amplifyframework.statemachine.codegen.events.AuthEvent import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent @@ -493,114 +484,6 @@ internal class RealAWSCognitoAuthPlugin( ) } - fun signOut(onComplete: Consumer) { - signOut(AuthSignOutOptions.builder().build(), onComplete) - } - - fun signOut(options: AuthSignOutOptions, onComplete: Consumer) { - authStateMachine.getCurrentState { authState -> - when (authState.authNState) { - is AuthenticationState.NotConfigured -> - onComplete.accept(AWSCognitoAuthSignOutResult.CompleteSignOut) - // Continue sign out and clear auth or guest credentials - is AuthenticationState.SignedIn, is AuthenticationState.SignedOut -> { - // Send SignOut event here instead of OnSubscribedCallback handler to ensure we do not fire - // onComplete immediately, which would happen if calling signOut while signed out - val event = AuthenticationEvent( - AuthenticationEvent.EventType.SignOutRequested( - SignOutData( - options.isGlobalSignOut, - (options as? AWSCognitoAuthSignOutOptions)?.browserPackage - ) - ) - ) - authStateMachine.send(event) - _signOut(onComplete = onComplete) - } - is AuthenticationState.FederatedToIdentityPool -> { - onComplete.accept( - AWSCognitoAuthSignOutResult.FailedSignOut( - InvalidStateException( - "The user is currently federated to identity pool. " + - "You must call clearFederationToIdentityPool to clear credentials." - ) - ) - ) - } - else -> onComplete.accept( - AWSCognitoAuthSignOutResult.FailedSignOut(InvalidStateException()) - ) - } - } - } - - private fun _signOut(sendHubEvent: Boolean = true, onComplete: Consumer) { - val token = StateChangeListenerToken() - var cancellationException: UserCancelledException? = null - authStateMachine.listen( - token, - { authState -> - if (authState is AuthState.Configured) { - val (authNState, authZState) = authState - when { - authNState is AuthenticationState.SignedOut && authZState is AuthorizationState.Configured -> { - authStateMachine.cancel(token) - if (authNState.signedOutData.hasError) { - val signedOutData = authNState.signedOutData - onComplete.accept( - AWSCognitoAuthSignOutResult.PartialSignOut( - hostedUIError = signedOutData.hostedUIErrorData?.let { HostedUIError(it) }, - globalSignOutError = signedOutData.globalSignOutErrorData?.let { - GlobalSignOutError(it) - }, - revokeTokenError = signedOutData.revokeTokenErrorData?.let { - RevokeTokenError( - it - ) - } - ) - ) - if (sendHubEvent) { - sendHubEvent(AuthChannelEventName.SIGNED_OUT.toString()) - } - } else { - onComplete.accept(AWSCognitoAuthSignOutResult.CompleteSignOut) - if (sendHubEvent) { - sendHubEvent(AuthChannelEventName.SIGNED_OUT.toString()) - } - } - } - authNState is AuthenticationState.Error -> { - authStateMachine.cancel(token) - onComplete.accept( - AWSCognitoAuthSignOutResult.FailedSignOut( - CognitoAuthExceptionConverter.lookup(authNState.exception, "Sign out failed.") - ) - ) - } - authNState is AuthenticationState.SigningOut -> { - val state = authNState.signOutState - if (state is SignOutState.Error && state.exception is UserCancelledException) { - cancellationException = state.exception - } - } - authNState is AuthenticationState.SignedIn && cancellationException != null -> { - authStateMachine.cancel(token) - cancellationException?.let { - onComplete.accept(AWSCognitoAuthSignOutResult.FailedSignOut(it)) - } - } - else -> { - // No - op - } - } - } - }, - { - } - ) - } - private fun addAuthStateChangeListener() { authStateMachine.listen( StateChangeListenerToken(), @@ -732,46 +615,6 @@ internal class RealAWSCognitoAuthPlugin( ) } - fun clearFederationToIdentityPool(onSuccess: Action, onError: Consumer) { - authStateMachine.getCurrentState { authState -> - val authNState = authState.authNState - val authZState = authState.authZState - when { - authState is AuthState.Configured && - ( - authNState is AuthenticationState.FederatedToIdentityPool && - authZState is AuthorizationState.SessionEstablished - ) || - ( - authZState is AuthorizationState.Error && - authZState.exception is SessionError && - authZState.exception.amplifyCredential is AmplifyCredential.IdentityPoolFederated - ) -> { - val event = AuthenticationEvent(AuthenticationEvent.EventType.ClearFederationToIdentityPool()) - authStateMachine.send(event) - _clearFederationToIdentityPool(onSuccess, onError) - } - else -> { - onError.accept(InvalidStateException("Clearing of federation failed.")) - } - } - } - } - - private fun _clearFederationToIdentityPool(onSuccess: Action, onError: Consumer) { - _signOut(sendHubEvent = false) { - when (it) { - is AWSCognitoAuthSignOutResult.FailedSignOut -> { - onError.accept(it.exception) - } - else -> { - onSuccess.call() - sendHubEvent(AWSCognitoAuthChannelEventName.FEDERATION_TO_IDENTITY_POOL_CLEARED.toString()) - } - } - } - } - private fun sendHubEvent(eventName: String) { Amplify.Hub.publish(HubChannel.AUTH, HubEvent.create(eventName)) } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt index 6e6f7a99f..e38f36c83 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt @@ -167,4 +167,13 @@ internal class AuthUseCaseFactory( ) fun confirmSignIn() = ConfirmSignInUseCase(stateMachine = stateMachine) + + fun signOut() = SignOutUseCase( + stateMachine = stateMachine + ) + + fun clearFederationToIdentityPool() = ClearFederationToIdentityPoolUseCase( + stateMachine = stateMachine, + signOut = signOut() + ) } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCase.kt new file mode 100644 index 000000000..c6c47ccc3 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCase.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito.usecases + +import com.amplifyframework.auth.cognito.AWSCognitoAuthChannelEventName +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter +import com.amplifyframework.statemachine.codegen.data.AmplifyCredential +import com.amplifyframework.statemachine.codegen.errors.SessionError +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.states.AuthState +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import com.amplifyframework.statemachine.codegen.states.AuthorizationState + +internal class ClearFederationToIdentityPoolUseCase( + private val stateMachine: AuthStateMachine, + private val signOut: SignOutUseCase, + private val emitter: AuthHubEventEmitter = AuthHubEventEmitter() +) { + suspend fun execute() { + val authState = stateMachine.getCurrentState() + + when { + authState.isFederatedToIdentityPool() -> { + val event = AuthenticationEvent(AuthenticationEvent.EventType.ClearFederationToIdentityPool()) + when (val result = signOut.completeSignOut(event = event, sendHubEvent = false)) { + is AWSCognitoAuthSignOutResult.FailedSignOut -> throw result.exception + else -> emitter.sendHubEvent( + AWSCognitoAuthChannelEventName.FEDERATION_TO_IDENTITY_POOL_CLEARED.toString() + ) + } + } + else -> throw InvalidStateException("Clearing of federation failed.") + } + } + + private fun AuthState.isFederatedToIdentityPool(): Boolean { + val authNState = this.authNState + val authZState = this.authZState + + return this is AuthState.Configured && + ( + authNState is AuthenticationState.FederatedToIdentityPool && + authZState is AuthorizationState.SessionEstablished + ) || + ( + authZState is AuthorizationState.Error && + authZState.exception is SessionError && + authZState.exception.amplifyCredential is AmplifyCredential.IdentityPoolFederated + ) + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCase.kt new file mode 100644 index 000000000..ba56e14ee --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCase.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito.usecases + +import com.amplifyframework.auth.AuthChannelEventName +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.CognitoAuthExceptionConverter +import com.amplifyframework.auth.cognito.exceptions.service.UserCancelledException +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions +import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult +import com.amplifyframework.auth.cognito.result.GlobalSignOutError +import com.amplifyframework.auth.cognito.result.HostedUIError +import com.amplifyframework.auth.cognito.result.RevokeTokenError +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.options.AuthSignOutOptions +import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter +import com.amplifyframework.auth.result.AuthSignOutResult +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.SignOutData +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.states.AuthState +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import com.amplifyframework.statemachine.codegen.states.AuthorizationState +import com.amplifyframework.statemachine.codegen.states.SignOutState +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onSubscription + +internal class SignOutUseCase( + private val stateMachine: AuthStateMachine, + private val emitter: AuthHubEventEmitter = AuthHubEventEmitter() +) { + + suspend fun execute(options: AuthSignOutOptions = AuthSignOutOptions.builder().build()): AuthSignOutResult { + val authState = stateMachine.getCurrentState() + return when (authState.authNState) { + is AuthenticationState.NotConfigured -> AWSCognitoAuthSignOutResult.CompleteSignOut + // Continue sign out and clear auth or guest credentials + is AuthenticationState.SignedIn, is AuthenticationState.SignedOut -> { + completeSignOut( + event = createSignOutEvent(options), + sendHubEvent = true + ) + } + is AuthenticationState.FederatedToIdentityPool -> { + AWSCognitoAuthSignOutResult.FailedSignOut( + InvalidStateException( + "The user is currently federated to identity pool. " + + "You must call clearFederationToIdentityPool to clear credentials." + ) + ) + } + else -> AWSCognitoAuthSignOutResult.FailedSignOut(InvalidStateException()) + } + } + + suspend fun completeSignOut(event: StateMachineEvent, sendHubEvent: Boolean): AuthSignOutResult { + var cancellationException: UserCancelledException? = null + + val result = stateMachine.state + .onSubscription { stateMachine.send(event) } + .drop(1) // Ignore current state + .mapNotNull { authState -> + if (authState !is AuthState.Configured) { + return@mapNotNull null + } + + val (authNState, authZState) = authState + + when { + authNState is AuthenticationState.SignedOut && authZState is AuthorizationState.Configured -> { + if (sendHubEvent) { + emitter.sendHubEvent(AuthChannelEventName.SIGNED_OUT.toString()) + } + if (authNState.signedOutData.hasError) { + val signedOutData = authNState.signedOutData + AWSCognitoAuthSignOutResult.PartialSignOut( + hostedUIError = signedOutData.hostedUIErrorData?.let { HostedUIError(it) }, + globalSignOutError = signedOutData.globalSignOutErrorData?.let { + GlobalSignOutError(it) + }, + revokeTokenError = signedOutData.revokeTokenErrorData?.let { RevokeTokenError(it) } + ) + } else { + AWSCognitoAuthSignOutResult.CompleteSignOut + } + } + authNState is AuthenticationState.Error -> { + AWSCognitoAuthSignOutResult.FailedSignOut( + CognitoAuthExceptionConverter.lookup(authNState.exception, "Sign out failed.") + ) + } + authNState is AuthenticationState.SigningOut -> { + val state = authNState.signOutState + if (state is SignOutState.Error && state.exception is UserCancelledException) { + cancellationException = state.exception + } + null + } + authNState is AuthenticationState.SignedIn && cancellationException != null -> { + AWSCognitoAuthSignOutResult.FailedSignOut(cancellationException!!) + } + else -> null // no-op + } + }.first() + + return result + } + + private fun createSignOutEvent(options: AuthSignOutOptions): StateMachineEvent = AuthenticationEvent( + AuthenticationEvent.EventType.SignOutRequested( + SignOutData( + options.isGlobalSignOut, + (options as? AWSCognitoAuthSignOutOptions)?.browserPackage + ) + ) + ) +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt index da2bc3191..08a58b436 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt @@ -618,9 +618,10 @@ class AWSCognitoAuthPluginTest { fun verifySignOut() { val expectedOnComplete = Consumer { } + val useCase = authPlugin.useCaseFactory.signOut() authPlugin.signOut(expectedOnComplete) - verify(timeout = CHANNEL_TIMEOUT) { realPlugin.signOut(any()) } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute() } } @Test @@ -628,9 +629,10 @@ class AWSCognitoAuthPluginTest { val expectedOptions = AuthSignOutOptions.builder().build() val expectedOnComplete = Consumer { } + val useCase = authPlugin.useCaseFactory.signOut() authPlugin.signOut(expectedOptions, expectedOnComplete) - verify(timeout = CHANNEL_TIMEOUT) { realPlugin.signOut(expectedOptions, any()) } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute(expectedOptions) } } @Test @@ -694,9 +696,10 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Action { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.clearFederationToIdentityPool() authPlugin.clearFederationToIdentityPool(expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { realPlugin.clearFederationToIdentityPool(any(), any()) } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute() } } @Test diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt index 41feb937c..17f9f72d1 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthValidationTest.kt @@ -27,6 +27,7 @@ import com.amplifyframework.auth.AuthException import com.amplifyframework.auth.cognito.featuretest.generators.authstategenerators.AuthStateJsonGenerator.DUMMY_TOKEN import com.amplifyframework.auth.cognito.helpers.AuthHelper import com.amplifyframework.auth.cognito.usecases.SignInUseCase +import com.amplifyframework.auth.cognito.usecases.SignOutUseCase import com.amplifyframework.auth.exceptions.InvalidStateException import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.core.Consumer @@ -142,6 +143,10 @@ class AuthValidationTest { configuration = configuration ) + private val signOutUseCase = SignOutUseCase( + stateMachine = stateMachine + ) + private val mainThreadSurrogate = newSingleThreadContext("Main thread") //region Setup/Teardown @@ -461,9 +466,7 @@ class AuthValidationTest { } } - private fun signOut() = blockForResult { complete -> - plugin.signOut(complete) - } + private fun signOut() = runBlocking { withTimeout(100000L) { signOutUseCase.execute() } } private fun signInHostedUi(): AuthSignInResult { every { hostedUIClient.launchCustomTabsSignIn(any()) } answers { @@ -478,9 +481,7 @@ class AuthValidationTest { } } - private fun signOutHostedUi() = blockForResult { complete -> - plugin.signOut(complete) - } + private fun signOutHostedUi() = signOut() private fun assertSignedOut() { val result = blockForResult { continuation -> stateMachine.getCurrentState { continuation.accept(it) } } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCaseTest.kt new file mode 100644 index 000000000..55efa1a8a --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCaseTest.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito.usecases + +import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult +import com.amplifyframework.auth.cognito.testUtil.authState +import com.amplifyframework.auth.cognito.testUtil.withAuthEvent +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter +import com.amplifyframework.statemachine.codegen.data.AmplifyCredential +import com.amplifyframework.statemachine.codegen.errors.SessionError +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.states.AuthState +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import com.amplifyframework.statemachine.codegen.states.AuthorizationState +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ClearFederationToIdentityPoolUseCaseTest { + private val credential = AmplifyCredential.IdentityPoolFederated(mockk(), "id", mockk()) + private val stateFlow = MutableStateFlow( + authState( + authNState = AuthenticationState.FederatedToIdentityPool(), + authZState = AuthorizationState.SessionEstablished(credential) + ) + ) + + private val stateMachine: AuthStateMachine = mockk { + every { state } returns stateFlow + every { stateTransitions } answers { stateFlow.drop(1) } + coEvery { getCurrentState() } answers { stateFlow.value } + } + + private val signOut: SignOutUseCase = mockk { + coEvery { completeSignOut(any(), any()) } returns AWSCognitoAuthSignOutResult.CompleteSignOut + } + private val emitter: AuthHubEventEmitter = mockk(relaxed = true) + + private val useCase = ClearFederationToIdentityPoolUseCase( + stateMachine = stateMachine, + signOut = signOut, + emitter = emitter + ) + + @Test + fun `throws InvalidStateException if not federated sign in`() = runTest { + stateFlow.value = authState(authNState = AuthenticationState.SignedIn(mockk(), mockk())) + + shouldThrow { + useCase.execute() + } + } + + @Test + fun `calls sign out if federated sign in`() = runTest { + useCase.execute() + + coVerify { + signOut.completeSignOut(withAuthEvent(), any()) + } + } + + @Test + fun `calls sign out if error state for federated sign in`() = runTest { + val exception = Exception() + stateFlow.value = authState( + authZState = AuthorizationState.Error(exception = SessionError(exception, credential)) + ) + + useCase.execute() + + coVerify { + signOut.completeSignOut(withAuthEvent(), any()) + } + } + + @Test + fun `throws exception from failed sign out`() = runTest { + val exception = AuthException("failed", "test") + coEvery { signOut.completeSignOut(any(), any()) } returns AWSCognitoAuthSignOutResult.FailedSignOut(exception) + + shouldThrowAny { useCase.execute() } shouldBe exception + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCaseTest.kt new file mode 100644 index 000000000..e3b207f9d --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCaseTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.amplifyframework.auth.cognito.usecases + +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.exceptions.service.UserCancelledException +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions +import com.amplifyframework.auth.cognito.result.AWSCognitoAuthSignOutResult +import com.amplifyframework.auth.cognito.testUtil.authState +import com.amplifyframework.auth.cognito.testUtil.withAuthEvent +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter +import com.amplifyframework.statemachine.codegen.data.HostedUIErrorData +import com.amplifyframework.statemachine.codegen.data.SignedOutData +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.states.AuthState +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import com.amplifyframework.statemachine.codegen.states.AuthorizationState +import com.amplifyframework.statemachine.codegen.states.SignOutState +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.coEvery +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class SignOutUseCaseTest { + private val stateFlow = MutableStateFlow( + authState( + authNState = AuthenticationState.SignedIn(mockk(), mockk()) + ) + ) + + private val stateMachine: AuthStateMachine = mockk { + every { state } returns stateFlow + every { stateTransitions } answers { stateFlow.drop(1) } + coEvery { getCurrentState() } answers { stateFlow.value } + justRun { send(any()) } + } + + private val emitter: AuthHubEventEmitter = mockk(relaxed = true) + + private val useCase = SignOutUseCase( + stateMachine = stateMachine, + emitter = emitter + ) + + @Test + fun `sends sign out event`() = runTest { + backgroundScope.launch { useCase.execute() } + runCurrent() + + verify { + stateMachine.send( + withAuthEvent { event -> + event.signOutData.globalSignOut shouldBe false + event.signOutData.browserPackage shouldBe null + } + ) + } + } + + @Test + fun `uses supplied options in sign out event`() = runTest { + val options = AWSCognitoAuthSignOutOptions.builder() + .globalSignOut(true) + .browserPackage("foo") + .build() + + backgroundScope.launch { useCase.execute(options) } + runCurrent() + + verify { + stateMachine.send( + withAuthEvent { event -> + event.signOutData.globalSignOut shouldBe true + event.signOutData.browserPackage shouldBe "foo" + } + ) + } + } + + @Test + fun `succeeds if not configured`() = runTest { + stateFlow.value = authState(authNState = AuthenticationState.NotConfigured()) + + val result = useCase.execute() + + result.shouldBeInstanceOf() + } + + @Test + fun `fails if sign in is federated`() = runTest { + stateFlow.value = authState(authNState = AuthenticationState.FederatedToIdentityPool()) + + val result = useCase.execute() + + val failed = result.shouldBeInstanceOf() + failed.exception.shouldBeInstanceOf() + } + + @Test + fun `fails if in unexpected state`() = runTest { + stateFlow.value = authState(authNState = AuthenticationState.SigningIn()) + + val result = useCase.execute() + + val failed = result.shouldBeInstanceOf() + failed.exception.shouldBeInstanceOf() + } + + @Test + fun `fails if user cancels sign out`() = runTest { + val deferred = backgroundScope.async { useCase.execute() } + runCurrent() + + val exception = UserCancelledException("failed", "test") + stateFlow.value = authState(authNState = AuthenticationState.SigningOut(SignOutState.Error(exception))) + runCurrent() + + stateFlow.value = authState(authNState = AuthenticationState.SignedIn(mockk(), mockk())) + + val result = deferred.await() + val failed = result.shouldBeInstanceOf() + failed.exception shouldBe exception + } + + @Test + fun `fails if reaching error state`() = runTest { + val deferred = backgroundScope.async { useCase.execute() } + runCurrent() + + val exception = Exception() + stateFlow.value = authState(authNState = AuthenticationState.Error(exception = exception)) + + val result = deferred.await() + val failed = result.shouldBeInstanceOf() + failed.exception.cause shouldBe exception + } + + @Test + fun `returns complete result`() = runTest { + val deferred = backgroundScope.async { useCase.execute() } + runCurrent() + + val signedOutData = SignedOutData() + + stateFlow.value = authState( + authNState = AuthenticationState.SignedOut(signedOutData), + authZState = AuthorizationState.Configured() + ) + + val result = deferred.await() + result shouldBe AWSCognitoAuthSignOutResult.CompleteSignOut + } + + @Test + fun `returns partial result`() = runTest { + val deferred = backgroundScope.async { useCase.execute() } + runCurrent() + + val exception = Exception() + val signedOutData = SignedOutData( + hostedUIErrorData = HostedUIErrorData("url", exception) + ) + + stateFlow.value = authState( + authNState = AuthenticationState.SignedOut(signedOutData), + authZState = AuthorizationState.Configured() + ) + + val result = deferred.await() + val partial = result.shouldBeInstanceOf() + + partial.hostedUIError?.url shouldBe "url" + partial.hostedUIError?.exception shouldBe exception + } +}