Skip to content

chore(auth): Move SignOut to usecase #3025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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() }
Original file line number Diff line number Diff line change
@@ -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) }
)
}
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -164,4 +164,13 @@ internal class AuthUseCaseFactory(
fetchAuthSession = fetchAuthSession(),
stateMachine = stateMachine
)

fun signOut() = SignOutUseCase(
stateMachine = stateMachine
)

fun clearFederationToIdentityPool() = ClearFederationToIdentityPoolUseCase(
stateMachine = stateMachine,
signOut = signOut()
)
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -624,19 +624,21 @@ 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
fun verifyOverloadedSignOut() {
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
Original file line number Diff line number Diff line change
@@ -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(100000L) { 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) } }
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}