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 4ce049f757..79e45b8201 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 @@ -260,7 +260,9 @@ class AWSCognitoAuthPlugin : AuthPlugin() { callingActivity: Activity, onSuccess: Consumer, onError: Consumer - ) = enqueue(onSuccess, onError) { queueFacade.signInWithSocialWebUI(provider, callingActivity) } + ) = enqueue(onSuccess, onError) { + useCaseFactory.webUiSignIn().execute(callingActivity, provider) + } override fun signInWithSocialWebUI( provider: AuthProvider, @@ -268,20 +270,26 @@ class AWSCognitoAuthPlugin : AuthPlugin() { options: AuthWebUISignInOptions, onSuccess: Consumer, onError: Consumer - ) = enqueue(onSuccess, onError) { queueFacade.signInWithSocialWebUI(provider, callingActivity, options) } + ) = enqueue(onSuccess, onError) { + useCaseFactory.webUiSignIn().execute(callingActivity, provider, options) + } override fun signInWithWebUI( callingActivity: Activity, onSuccess: Consumer, onError: Consumer - ) = enqueue(onSuccess, onError) { queueFacade.signInWithWebUI(callingActivity) } + ) = enqueue(onSuccess, onError) { + useCaseFactory.webUiSignIn().execute(callingActivity = callingActivity) + } override fun signInWithWebUI( callingActivity: Activity, options: AuthWebUISignInOptions, onSuccess: Consumer, onError: Consumer - ) = enqueue(onSuccess, onError) { queueFacade.signInWithWebUI(callingActivity, options) } + ) = enqueue(onSuccess, onError) { + useCaseFactory.webUiSignIn().execute(callingActivity = callingActivity, options = options) + } override fun handleWebUISignInResponse(intent: Intent?) { pluginScope.launch { 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 39c0874643..19a679fbbf 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 @@ -15,60 +15,14 @@ package com.amplifyframework.auth.cognito -import android.app.Activity -import com.amplifyframework.auth.AuthProvider import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.options.AuthFetchSessionOptions -import com.amplifyframework.auth.options.AuthWebUISignInOptions -import com.amplifyframework.auth.result.AuthSignInResult import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuthPlugin) { - suspend fun signInWithSocialWebUI(provider: AuthProvider, callingActivity: Activity): AuthSignInResult = - suspendCoroutine { continuation -> - delegate.signInWithSocialWebUI( - provider, - callingActivity, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } - - suspend fun signInWithSocialWebUI( - provider: AuthProvider, - callingActivity: Activity, - options: AuthWebUISignInOptions - ): AuthSignInResult = suspendCoroutine { continuation -> - delegate.signInWithSocialWebUI( - provider, - callingActivity, - options, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } - - suspend fun signInWithWebUI(callingActivity: Activity): AuthSignInResult = suspendCoroutine { continuation -> - delegate.signInWithWebUI( - callingActivity, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } - - suspend fun signInWithWebUI(callingActivity: Activity, options: AuthWebUISignInOptions): AuthSignInResult = - suspendCoroutine { continuation -> - delegate.signInWithWebUI( - callingActivity, - options, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } - suspend fun fetchAuthSession(): AuthSession = suspendCoroutine { continuation -> delegate.fetchAuthSession( { continuation.resume(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 50d16a0baf..cb1e6904c6 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 @@ -15,21 +15,14 @@ package com.amplifyframework.auth.cognito -import android.app.Activity import androidx.annotation.WorkerThread import com.amplifyframework.AmplifyException import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.auth.AWSCognitoAuthMetadataType import com.amplifyframework.auth.AuthChannelEventName import com.amplifyframework.auth.AuthException -import com.amplifyframework.auth.AuthProvider import com.amplifyframework.auth.AuthSession -import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidOauthConfigurationException -import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException -import com.amplifyframework.auth.cognito.exceptions.invalidstate.SignedInException import com.amplifyframework.auth.cognito.exceptions.service.InvalidAccountTypeException -import com.amplifyframework.auth.cognito.helpers.HostedUIHelper -import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions import com.amplifyframework.auth.exceptions.ConfigurationException import com.amplifyframework.auth.exceptions.InvalidStateException import com.amplifyframework.auth.exceptions.NotAuthorizedException @@ -38,10 +31,6 @@ 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.AuthWebUISignInOptions -import com.amplifyframework.auth.result.AuthSignInResult -import com.amplifyframework.auth.result.step.AuthNextSignInStep -import com.amplifyframework.auth.result.step.AuthSignInStep import com.amplifyframework.core.Amplify import com.amplifyframework.core.Consumer import com.amplifyframework.hub.HubChannel @@ -49,15 +38,11 @@ import com.amplifyframework.hub.HubEvent import com.amplifyframework.logging.Logger import com.amplifyframework.statemachine.StateChangeListenerToken import com.amplifyframework.statemachine.codegen.data.AmplifyCredential -import com.amplifyframework.statemachine.codegen.data.SignInData import com.amplifyframework.statemachine.codegen.errors.SessionError import com.amplifyframework.statemachine.codegen.events.AuthEvent -import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent import com.amplifyframework.statemachine.codegen.events.AuthorizationEvent 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.HostedUISignInState import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlinx.coroutines.flow.collect @@ -111,172 +96,6 @@ internal class RealAWSCognitoAuthPlugin( authStateMachine.state.takeWhile { it !is AuthState.Configured && it !is AuthState.Error }.collect() } - fun signInWithSocialWebUI( - provider: AuthProvider, - callingActivity: Activity, - onSuccess: Consumer, - onError: Consumer - ) { - signInWithSocialWebUI( - provider, - callingActivity, - AWSCognitoAuthWebUISignInOptions.builder().build(), - onSuccess, - onError - ) - } - - fun signInWithSocialWebUI( - provider: AuthProvider, - callingActivity: Activity, - options: AuthWebUISignInOptions, - onSuccess: Consumer, - onError: Consumer - ) { - signInWithHostedUI( - provider = provider, - callingActivity = callingActivity, - options = options, - onSuccess = onSuccess, - onError = onError - ) - } - - fun signInWithWebUI( - callingActivity: Activity, - onSuccess: Consumer, - onError: Consumer - ) { - signInWithWebUI(callingActivity, AuthWebUISignInOptions.builder().build(), onSuccess, onError) - } - - fun signInWithWebUI( - callingActivity: Activity, - options: AuthWebUISignInOptions, - onSuccess: Consumer, - onError: Consumer - ) { - signInWithHostedUI( - callingActivity = callingActivity, - options = options, - onSuccess = onSuccess, - onError = onError - ) - } - - private fun signInWithHostedUI( - provider: AuthProvider? = null, - callingActivity: Activity, - options: AuthWebUISignInOptions, - onSuccess: Consumer, - onError: Consumer - ) { - authStateMachine.getCurrentState { authState -> - when (authState.authNState) { - is AuthenticationState.NotConfigured -> onError.accept( - InvalidUserPoolConfigurationException() - ) - // Continue sign in - is AuthenticationState.SignedOut -> { - if (configuration.oauth == null) { - onError.accept(InvalidOauthConfigurationException()) - return@getCurrentState - } - - _signInWithHostedUI( - callingActivity = callingActivity, - options = options, - onSuccess = onSuccess, - onError = onError, - provider = provider - ) - } - is AuthenticationState.SignedIn -> onError.accept(SignedInException()) - is AuthenticationState.SigningIn -> { - val token = StateChangeListenerToken() - authStateMachine.listen( - token, - { authState -> - when (authState.authNState) { - is AuthenticationState.SignedOut -> { - authStateMachine.cancel(token) - _signInWithHostedUI( - callingActivity = callingActivity, - options = options, - onSuccess = onSuccess, - onError = onError, - provider = provider - ) - } - else -> Unit - } - }, - { - authStateMachine.send(AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn())) - } - ) - } - else -> onError.accept(InvalidStateException()) - } - } - } - - private fun _signInWithHostedUI( - callingActivity: Activity, - options: AuthWebUISignInOptions, - onSuccess: Consumer, - onError: Consumer, - provider: AuthProvider? = null - ) { - val token = StateChangeListenerToken() - authStateMachine.listen( - token, - { authState -> - val authNState = authState.authNState - val authZState = authState.authZState - when { - authNState is AuthenticationState.SigningIn -> { - val hostedUISignInState = authNState.signInState.hostedUISignInState - if (hostedUISignInState is HostedUISignInState.Error) { - authStateMachine.cancel(token) - val exception = hostedUISignInState.exception - onError.accept( - if (exception is AuthException) { - exception - } else { - UnknownException("Sign in failed", exception) - } - ) - authStateMachine.send(AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn())) - } - } - authNState is AuthenticationState.SignedIn && - authZState is AuthorizationState.SessionEstablished -> { - authStateMachine.cancel(token) - val authSignInResult = - AuthSignInResult( - true, - AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null, null) - ) - onSuccess.accept(authSignInResult) - sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) - } - else -> Unit - } - }, - { - val hostedUIOptions = HostedUIHelper.createHostedUIOptions(callingActivity, provider, options) - authStateMachine.send( - AuthenticationEvent( - AuthenticationEvent.EventType.SignInRequested( - SignInData.HostedUISignInData(hostedUIOptions) - ) - ) - ) - } - ) - } - fun fetchAuthSession(onSuccess: Consumer, onError: Consumer) { fetchAuthSession(AuthFetchSessionOptions.defaults(), onSuccess, onError) } 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 cf0295aea3..8f72ab13b4 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 @@ -179,6 +179,11 @@ internal class AuthUseCaseFactory( signOut = signOut() ) + fun webUiSignIn() = WebUiSignInUseCase( + stateMachine = stateMachine, + configuration = authEnvironment.configuration + ) + fun webUISignInResponse() = WebUiSignInResponseUseCase( stateMachine = stateMachine, authEnvironment = authEnvironment diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/WebUiSignInUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/WebUiSignInUseCase.kt new file mode 100644 index 0000000000..a677537e8b --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/WebUiSignInUseCase.kt @@ -0,0 +1,114 @@ +/* + * 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 android.app.Activity +import com.amplifyframework.auth.AuthChannelEventName +import com.amplifyframework.auth.AuthProvider +import com.amplifyframework.auth.cognito.AuthConfiguration +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.CognitoAuthExceptionConverter.Companion.toAuthException +import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidOauthConfigurationException +import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException +import com.amplifyframework.auth.cognito.exceptions.invalidstate.SignedInException +import com.amplifyframework.auth.cognito.helpers.HostedUIHelper +import com.amplifyframework.auth.cognito.options.AWSCognitoAuthWebUISignInOptions +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.options.AuthWebUISignInOptions +import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter +import com.amplifyframework.auth.result.AuthSignInResult +import com.amplifyframework.auth.result.step.AuthNextSignInStep +import com.amplifyframework.auth.result.step.AuthSignInStep +import com.amplifyframework.statemachine.codegen.data.SignInData +import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import com.amplifyframework.statemachine.codegen.states.AuthorizationState +import com.amplifyframework.statemachine.codegen.states.HostedUISignInState +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onSubscription + +internal class WebUiSignInUseCase( + private val stateMachine: AuthStateMachine, + private val configuration: AuthConfiguration, + private val emitter: AuthHubEventEmitter = AuthHubEventEmitter() +) { + suspend fun execute( + callingActivity: Activity, + provider: AuthProvider? = null, + options: AuthWebUISignInOptions = AWSCognitoAuthWebUISignInOptions.builder().build() + ): AuthSignInResult { + waitForStateThatAllowsSignIn() + + if (configuration.oauth == null) { + throw InvalidOauthConfigurationException() + } + + val hostedUIOptions = HostedUIHelper.createHostedUIOptions(callingActivity, provider, options) + val event = AuthenticationEvent( + AuthenticationEvent.EventType.SignInRequested( + SignInData.HostedUISignInData(hostedUIOptions) + ) + ) + + val result = stateMachine.state + .onSubscription { stateMachine.send(event) } + .drop(1) + .mapNotNull { authState -> + val authNState = authState.authNState + val authZState = authState.authZState + when { + authNState is AuthenticationState.SigningIn -> { + val hostedUISignInState = authNState.signInState.hostedUISignInState + if (hostedUISignInState is HostedUISignInState.Error) { + stateMachine.send( + AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn()) + ) + throw hostedUISignInState.exception.toAuthException("Sign in failed") + } + null + } + authNState is AuthenticationState.SignedIn && + authZState is AuthorizationState.SessionEstablished -> { + AuthSignInResult( + true, + AuthNextSignInStep(AuthSignInStep.DONE, mapOf(), null, null, null, null) + ) + } + else -> null + } + }.first() + + emitter.sendHubEvent(AuthChannelEventName.SIGNED_IN.toString()) + return result + } + + private suspend fun waitForStateThatAllowsSignIn() { + stateMachine.state.mapNotNull { authState -> + when (authState.authNState) { + is AuthenticationState.NotConfigured -> throw InvalidUserPoolConfigurationException() + is AuthenticationState.SignedOut -> authState + is AuthenticationState.SignedIn -> throw SignedInException() + is AuthenticationState.SigningIn -> { + stateMachine.send(AuthenticationEvent(AuthenticationEvent.EventType.CancelSignIn())) + null + } + else -> throw InvalidStateException() + } + }.first() + } +} 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 d559ef2515..28aeafa0be 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 @@ -232,15 +232,12 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Consumer { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.webUiSignIn() + authPlugin.signInWithSocialWebUI(expectedProvider, expectedActivity, expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { - realPlugin.signInWithSocialWebUI( - expectedProvider, - expectedActivity, - any(), - any() - ) + coVerify(timeout = CHANNEL_TIMEOUT) { + useCase.execute(expectedActivity, expectedProvider) } } @@ -252,6 +249,8 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Consumer { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.webUiSignIn() + authPlugin.signInWithSocialWebUI( expectedProvider, expectedActivity, @@ -260,14 +259,8 @@ class AWSCognitoAuthPluginTest { expectedOnError ) - verify(timeout = CHANNEL_TIMEOUT) { - realPlugin.signInWithSocialWebUI( - expectedProvider, - expectedActivity, - expectedOptions, - any(), - any() - ) + coVerify(timeout = CHANNEL_TIMEOUT) { + useCase.execute(expectedActivity, expectedProvider, expectedOptions) } } @@ -277,9 +270,11 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Consumer { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.webUiSignIn() + authPlugin.signInWithWebUI(expectedActivity, expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { realPlugin.signInWithWebUI(expectedActivity, any(), any()) } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute(expectedActivity) } } @Test @@ -289,10 +284,12 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Consumer { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.webUiSignIn() + authPlugin.signInWithWebUI(expectedActivity, expectedOptions, expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { - realPlugin.signInWithWebUI(expectedActivity, expectedOptions, any(), any()) + coVerify(timeout = CHANNEL_TIMEOUT) { + useCase.execute(callingActivity = expectedActivity, options = expectedOptions) } } 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 c24626f9cd..8460c95b13 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 @@ -29,6 +29,7 @@ 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.cognito.usecases.WebUiSignInResponseUseCase +import com.amplifyframework.auth.cognito.usecases.WebUiSignInUseCase import com.amplifyframework.auth.exceptions.InvalidStateException import com.amplifyframework.auth.result.AuthSignInResult import com.amplifyframework.core.Consumer @@ -148,6 +149,11 @@ class AuthValidationTest { stateMachine = stateMachine ) + private val webUiSignInUseCase = WebUiSignInUseCase( + stateMachine = stateMachine, + configuration = environment.configuration + ) + private val webUiSignInResponseUseCase = WebUiSignInResponseUseCase( stateMachine = stateMachine, authEnvironment = environment @@ -480,8 +486,8 @@ class AuthValidationTest { webUiSignInResponseUseCase.execute(mockk(relaxed = true)) } } - return blockForResult { success, error -> - plugin.signInWithWebUI(activity, success, error) + return runBlocking { + webUiSignInUseCase.execute(activity) } } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/WebUiSignInUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/WebUiSignInUseCaseTest.kt new file mode 100644 index 0000000000..823730e858 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/WebUiSignInUseCaseTest.kt @@ -0,0 +1,309 @@ +/* + * 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 android.app.Activity +import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.AuthProvider +import com.amplifyframework.auth.cognito.AuthConfiguration +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidOauthConfigurationException +import com.amplifyframework.auth.cognito.exceptions.configuration.InvalidUserPoolConfigurationException +import com.amplifyframework.auth.cognito.exceptions.invalidstate.SignedInException +import com.amplifyframework.auth.cognito.testUtil.authState +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.exceptions.UnknownException +import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter +import com.amplifyframework.auth.result.step.AuthSignInStep +import com.amplifyframework.statemachine.StateMachineEvent +import com.amplifyframework.statemachine.codegen.data.OauthConfiguration +import com.amplifyframework.statemachine.codegen.data.SignInData +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.HostedUISignInState +import com.amplifyframework.statemachine.codegen.states.SignInState +import io.kotest.assertions.throwables.shouldThrow +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.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class WebUiSignInUseCaseTest { + + private val callingActivity: Activity = mockk() + + private val oauthConfig: OauthConfiguration = mockk() + + private val configuration: AuthConfiguration = mockk { + every { oauth } returns oauthConfig + } + + private val stateFlow = MutableStateFlow( + authState( + authNState = AuthenticationState.SignedOut(SignedOutData()), + authZState = AuthorizationState.Configured() + ) + ) + + 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 = WebUiSignInUseCase( + stateMachine = stateMachine, + configuration = configuration, + emitter = emitter + ) + + @Test + fun `throws InvalidUserPoolConfigurationException when not configured`() = runTest { + stateFlow.value = authState( + authNState = AuthenticationState.NotConfigured(), + authZState = AuthorizationState.Configured() + ) + + shouldThrow { + useCase.execute(callingActivity = callingActivity) + } + } + + @Test + fun `throws SignedInException when already signed in`() = runTest { + stateFlow.value = authState( + authNState = AuthenticationState.SignedIn(mockk(), mockk()), + authZState = AuthorizationState.Configured() + ) + + shouldThrow { + useCase.execute(callingActivity = callingActivity) + } + } + + @Test + fun `throws InvalidOauthConfigurationException when OAuth config is null`() = runTest { + val nullOauthConfig: AuthConfiguration = mockk { + every { oauth } returns null + } + val useCaseNoOauth = WebUiSignInUseCase( + stateMachine = stateMachine, + configuration = nullOauthConfig, + emitter = emitter + ) + + shouldThrow { + useCaseNoOauth.execute(callingActivity = callingActivity) + } + } + + @Test + fun `sends SignInRequested with HostedUISignInData in SignedOut state`() = runTest { + backgroundScope.async { useCase.execute(provider = AuthProvider.google(), callingActivity = callingActivity) } + runCurrent() + + verify { + stateMachine.send( + withArg { + val event = it.shouldBeInstanceOf() + val type = event.eventType + .shouldBeInstanceOf() + type.signInData.shouldBeInstanceOf() + } + ) + } + } + + @Test + fun `cancels existing sign-in and proceeds when SigningIn`() = runTest { + stateFlow.value = authState( + authNState = AuthenticationState.SigningIn(SignInState.NotStarted()), + authZState = AuthorizationState.Configured() + ) + + val deferred = backgroundScope.async { + useCase.execute(callingActivity = callingActivity) + } + runCurrent() + + // Verify CancelSignIn was sent + verify { + stateMachine.send( + withArg { + val event = it.shouldBeInstanceOf() + event.eventType.shouldBeInstanceOf() + } + ) + } + + // Transition to SignedOut so the use case can proceed + stateFlow.value = authState( + authNState = AuthenticationState.SignedOut(SignedOutData()), + authZState = AuthorizationState.Configured() + ) + runCurrent() + + // Now transition to signed in to complete the flow + stateFlow.value = authState( + authNState = AuthenticationState.SignedIn(mockk(), mockk()), + authZState = AuthorizationState.SessionEstablished(mockk()) + ) + + val result = deferred.await() + result.isSignedIn shouldBe true + } + + @Test + fun `returns success result with isSignedIn true and DONE step`() = runTest { + val deferred = backgroundScope.async { + useCase.execute(callingActivity = callingActivity) + } + runCurrent() + + stateFlow.value = authState( + authNState = AuthenticationState.SignedIn(mockk(), mockk()), + authZState = AuthorizationState.SessionEstablished(mockk()) + ) + + val result = deferred.await() + result.isSignedIn shouldBe true + result.nextStep.signInStep shouldBe AuthSignInStep.DONE + } + + @Test + fun `publishes SIGNED_IN hub event on success`() = runTest { + val deferred = backgroundScope.async { + useCase.execute(callingActivity = callingActivity) + } + runCurrent() + + stateFlow.value = authState( + authNState = AuthenticationState.SignedIn(mockk(), mockk()), + authZState = AuthorizationState.SessionEstablished(mockk()) + ) + + deferred.await() + + verify { + emitter.sendHubEvent("SIGNED_IN") + } + } + + @Test + fun `throws AuthException on HostedUISignInState Error`() = runTest { + val authException = AuthException("Hosted UI error", "Try again") + + val supervisor = SupervisorJob(backgroundScope.coroutineContext[Job]) + val deferred = backgroundScope.async(supervisor) { + useCase.execute(callingActivity = callingActivity) + } + runCurrent() + + stateFlow.value = authState( + authNState = AuthenticationState.SigningIn( + SignInState.SigningInWithHostedUI(HostedUISignInState.Error(authException)) + ), + authZState = AuthorizationState.Configured() + ) + + val thrown = shouldThrow { + deferred.await() + } + thrown.message shouldBe "Hosted UI error" + } + + @Test + fun `wraps non-AuthException in UnknownException on HostedUISignInState Error`() = runTest { + val runtimeException = RuntimeException("something broke") + + val supervisor = SupervisorJob(backgroundScope.coroutineContext[Job]) + val deferred = backgroundScope.async(supervisor) { + useCase.execute(callingActivity = callingActivity) + } + runCurrent() + + stateFlow.value = authState( + authNState = AuthenticationState.SigningIn( + SignInState.SigningInWithHostedUI(HostedUISignInState.Error(runtimeException)) + ), + authZState = AuthorizationState.Configured() + ) + + shouldThrow { + deferred.await() + } + } + + @Test + fun `sends CancelSignIn on error`() = runTest { + val authException = AuthException("error", "recovery") + + val supervisor = SupervisorJob(backgroundScope.coroutineContext[Job]) + val deferred = backgroundScope.async(supervisor) { + useCase.execute(callingActivity = callingActivity) + } + runCurrent() + + stateFlow.value = authState( + authNState = AuthenticationState.SigningIn( + SignInState.SigningInWithHostedUI(HostedUISignInState.Error(authException)) + ), + authZState = AuthorizationState.Configured() + ) + + runCatching { deferred.await() } + + verify { + stateMachine.send( + withArg { + val event = it.shouldBeInstanceOf() + event.eventType.shouldBeInstanceOf() + } + ) + } + } + + @Test + fun `throws InvalidStateException for unexpected states`() = runTest { + stateFlow.value = authState( + authNState = AuthenticationState.Configured(), + authZState = AuthorizationState.Configured() + ) + + shouldThrow { + useCase.execute(callingActivity = callingActivity) + } + } +}