From d3150408d2fb261174dbf58f313e2230b1ee16b3 Mon Sep 17 00:00:00 2001
From: Matt Creaser <mattwcc@amazon.com>
Date: Tue, 1 Apr 2025 10:43:20 -0300
Subject: [PATCH 1/2] Move SignOut to usecase
---
.../auth/cognito/AWSCognitoAuthPlugin.kt | 6 +-
.../auth/cognito/KotlinAuthFacadeInternal.kt | 17 --
.../auth/cognito/RealAWSCognitoAuthPlugin.kt | 157 --------------
.../cognito/usecases/AuthUseCaseFactory.kt | 9 +
.../ClearFederationToIdentityPoolUseCase.kt | 69 ++++++
.../auth/cognito/usecases/SignOutUseCase.kt | 127 +++++++++++
.../auth/cognito/AWSCognitoAuthPluginTest.kt | 9 +-
.../auth/cognito/AuthValidationTest.kt | 13 +-
...learFederationToIdentityPoolUseCaseTest.kt | 110 ++++++++++
.../cognito/usecases/SignOutUseCaseTest.kt | 201 ++++++++++++++++++
10 files changed, 532 insertions(+), 186 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 38bbf0cdf..3be83e037 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<AWSCognitoAuthService>() {
override fun signOut(onComplete: Consumer<AuthSignOutResult>) = enqueue(
onComplete,
onError = ::throwIt
- ) { queueFacade.signOut() }
+ ) { useCaseFactory.signOut().execute() }
override fun signOut(options: AuthSignOutOptions, onComplete: Consumer<AuthSignOutResult>) = enqueue(
onComplete,
onError = ::throwIt
- ) { queueFacade.signOut(options) }
+ ) { useCaseFactory.signOut().execute(options) }
override fun deleteUser(onSuccess: Action, onError: Consumer<AuthException>) = enqueue(onSuccess, onError) {
useCaseFactory.deleteUser().execute()
@@ -523,7 +523,7 @@ class AWSCognitoAuthPlugin : AuthPlugin<AWSCognitoAuthService>() {
* @param onError Error callback
*/
fun clearFederationToIdentityPool(onSuccess: Action, onError: Consumer<AuthException>) =
- enqueue(onSuccess, onError) { queueFacade.clearFederationToIdentityPool() }
+ enqueue(onSuccess, onError) { useCaseFactory.clearFederationToIdentityPool().execute() }
fun fetchMFAPreference(onSuccess: Consumer<UserMFAPreference>, onError: Consumer<AuthException>) =
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 a2bc97903..ab1a7ea1d 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.cognito.options.FederateToIdentityPoolOptions
import com.amplifyframework.auth.cognito.result.FederateToIdentityPoolResult
import com.amplifyframework.auth.options.AuthConfirmSignInOptions
import com.amplifyframework.auth.options.AuthSignInOptions
-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
@@ -116,14 +114,6 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth
delegate.handleWebUISignInResponse(intent)
}
- 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,
@@ -137,11 +127,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 5d9d9d7ef..329fcebc4 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
@@ -46,26 +46,18 @@ import com.amplifyframework.auth.cognito.helpers.isMfaSetupSelectionChallenge
import com.amplifyframework.auth.cognito.helpers.value
import com.amplifyframework.auth.cognito.options.AWSCognitoAuthConfirmSignInOptions
import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignInOptions
-import com.amplifyframework.auth.cognito.options.AWSCognitoAuthSignOutOptions
import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions
import com.amplifyframework.auth.cognito.options.AuthFlowType
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.InvalidStateException
import com.amplifyframework.auth.exceptions.UnknownException
import com.amplifyframework.auth.options.AuthConfirmSignInOptions
import com.amplifyframework.auth.options.AuthSignInOptions
-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
@@ -78,7 +70,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.data.WebAuthnSignInContext
import com.amplifyframework.statemachine.codegen.data.challengeNameType
import com.amplifyframework.statemachine.codegen.errors.SessionError
@@ -980,114 +971,6 @@ internal class RealAWSCognitoAuthPlugin(
}
}
- fun signOut(onComplete: Consumer<AuthSignOutResult>) {
- signOut(AuthSignOutOptions.builder().build(), onComplete)
- }
-
- fun signOut(options: AuthSignOutOptions, onComplete: Consumer<AuthSignOutResult>) {
- 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<AuthSignOutResult>) {
- 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(),
@@ -1219,46 +1102,6 @@ internal class RealAWSCognitoAuthPlugin(
)
}
- fun clearFederationToIdentityPool(onSuccess: Action, onError: Consumer<AuthException>) {
- 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<AuthException>) {
- _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) {
if (lastPublishedHubEventName.get() != eventName) {
lastPublishedHubEventName.set(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 584cd2f68..1a3b6640e 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
@@ -164,4 +164,13 @@ internal class AuthUseCaseFactory(
fetchAuthSession = fetchAuthSession(),
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..a06360c3a
--- /dev/null
+++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCase.kt
@@ -0,0 +1,69 @@
+/*
+ * 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())
+ stateMachine.send(event)
+
+ when (val result = signOut.completeSignOut(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..d70501cde
--- /dev/null
+++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/SignOutUseCase.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.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.first
+import kotlinx.coroutines.flow.mapNotNull
+
+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 -> {
+ // 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
+ sendSignOutRequest(options)
+ completeSignOut(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(sendHubEvent: Boolean): AuthSignOutResult {
+ var cancellationException: UserCancelledException? = null
+
+ val result = stateMachine.stateTransitions.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 sendSignOutRequest(options: AuthSignOutOptions) {
+ val event = AuthenticationEvent(
+ AuthenticationEvent.EventType.SignOutRequested(
+ SignOutData(
+ options.isGlobalSignOut,
+ (options as? AWSCognitoAuthSignOutOptions)?.browserPackage
+ )
+ )
+ )
+ stateMachine.send(event)
+ }
+}
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 b6f872008..02ed29f21 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
@@ -624,9 +624,10 @@ class AWSCognitoAuthPluginTest {
fun verifySignOut() {
val expectedOnComplete = Consumer<AuthSignOutResult> { }
+ val useCase = authPlugin.useCaseFactory.signOut()
authPlugin.signOut(expectedOnComplete)
- verify(timeout = CHANNEL_TIMEOUT) { realPlugin.signOut(any()) }
+ coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute() }
}
@Test
@@ -634,9 +635,10 @@ class AWSCognitoAuthPluginTest {
val expectedOptions = AuthSignOutOptions.builder().build()
val expectedOnComplete = Consumer<AuthSignOutResult> { }
+ 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
@@ -700,9 +702,10 @@ class AWSCognitoAuthPluginTest {
val expectedOnSuccess = Action { }
val expectedOnError = Consumer<AuthException> { }
+ 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 0ec803337..2f0020811 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
@@ -26,6 +26,7 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.UserNotFoundExcepti
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.SignOutUseCase
import com.amplifyframework.auth.result.AuthSignInResult
import com.amplifyframework.core.Consumer
import com.amplifyframework.logging.Logger
@@ -134,6 +135,10 @@ class AuthValidationTest {
logger = logger
)
+ private val signOutUseCase = SignOutUseCase(
+ stateMachine = stateMachine
+ )
+
private val mainThreadSurrogate = newSingleThreadContext("Main thread")
//region Setup/Teardown
@@ -453,9 +458,7 @@ class AuthValidationTest {
}
}
- private fun signOut() = blockForResult { complete ->
- plugin.signOut(complete)
- }
+ private fun signOut() = runBlocking { withTimeout(10000L) { signOutUseCase.execute() } }
private fun signInHostedUi(): AuthSignInResult {
every { hostedUIClient.launchCustomTabsSignIn(any()) } answers {
@@ -470,9 +473,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..a81cbe6bd
--- /dev/null
+++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ClearFederationToIdentityPoolUseCaseTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.justRun
+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>(
+ 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 }
+ justRun { send(any()) }
+ }
+
+ private val signOut: SignOutUseCase = mockk {
+ coEvery { completeSignOut(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<InvalidStateException> {
+ useCase.execute()
+ }
+ }
+
+ @Test
+ fun `sends event if federated sign in`() = runTest {
+ useCase.execute()
+
+ coVerify {
+ stateMachine.send(withAuthEvent<AuthenticationEvent.EventType.ClearFederationToIdentityPool>())
+ }
+ }
+
+ @Test
+ fun `sends event if error state for federated sign in`() = runTest {
+ val exception = Exception()
+ stateFlow.value = authState(
+ authZState = AuthorizationState.Error(exception = SessionError(exception, credential))
+ )
+
+ useCase.execute()
+
+ coVerify {
+ stateMachine.send(withAuthEvent<AuthenticationEvent.EventType.ClearFederationToIdentityPool>())
+ }
+ }
+
+ @Test
+ fun `throws exception from failed sign out`() = runTest {
+ val exception = AuthException("failed", "test")
+ coEvery { signOut.completeSignOut(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>(
+ 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<AuthenticationEvent.EventType.SignOutRequested> { 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<AuthenticationEvent.EventType.SignOutRequested> { 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<AWSCognitoAuthSignOutResult.CompleteSignOut>()
+ }
+
+ @Test
+ fun `fails if sign in is federated`() = runTest {
+ stateFlow.value = authState(authNState = AuthenticationState.FederatedToIdentityPool())
+
+ val result = useCase.execute()
+
+ val failed = result.shouldBeInstanceOf<AWSCognitoAuthSignOutResult.FailedSignOut>()
+ failed.exception.shouldBeInstanceOf<InvalidStateException>()
+ }
+
+ @Test
+ fun `fails if in unexpected state`() = runTest {
+ stateFlow.value = authState(authNState = AuthenticationState.SigningIn())
+
+ val result = useCase.execute()
+
+ val failed = result.shouldBeInstanceOf<AWSCognitoAuthSignOutResult.FailedSignOut>()
+ failed.exception.shouldBeInstanceOf<InvalidStateException>()
+ }
+
+ @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<AWSCognitoAuthSignOutResult.FailedSignOut>()
+ 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<AWSCognitoAuthSignOutResult.FailedSignOut>()
+ 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<AWSCognitoAuthSignOutResult.PartialSignOut>()
+
+ partial.hostedUIError?.url shouldBe "url"
+ partial.hostedUIError?.exception shouldBe exception
+ }
+}
From f98699b4e12303fed4019fba15e92017648b00d3 Mon Sep 17 00:00:00 2001
From: Matt Creaser <mattwcc@amazon.com>
Date: Wed, 16 Apr 2025 15:45:10 -0300
Subject: [PATCH 2/2] Fix timeout in AuthValidationTest to match previous value
---
.../com/amplifyframework/auth/cognito/AuthValidationTest.kt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 2f0020811..d5b11de3d 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
@@ -458,7 +458,7 @@ class AuthValidationTest {
}
}
- private fun signOut() = runBlocking { withTimeout(10000L) { signOutUseCase.execute() } }
+ private fun signOut() = runBlocking { withTimeout(100000L) { signOutUseCase.execute() } }
private fun signInHostedUi(): AuthSignInResult {
every { hostedUIClient.launchCustomTabsSignIn(any()) } answers {