From 9dbd9f43cc3894d1b7508025f4fe2a30b19d06fa Mon Sep 17 00:00:00 2001 From: Matt Creaser Date: Mon, 3 Feb 2025 17:34:51 -0400 Subject: [PATCH] chore(auth): Create use cases for device management APIs (#2979) Co-authored-by: Tyler Roach --- .../auth/cognito/AWSCognitoAuthPlugin.kt | 8 +- .../auth/cognito/AuthStateMachine.kt | 10 ++ .../auth/cognito/KotlinAuthFacadeInternal.kt | 38 ----- .../auth/cognito/RealAWSCognitoAuthPlugin.kt | 134 ------------------ .../AssociateWebAuthnCredentialUseCase.kt | 5 +- .../cognito/usecases/AuthUseCaseFactory.kt | 20 +++ .../DeleteWebAuthnCredentialUseCase.kt | 5 +- .../cognito/usecases/FetchDevicesUseCase.kt | 39 +++++ .../cognito/usecases/ForgetDeviceUseCase.kt | 44 ++++++ .../ListWebAuthnCredentialsUseCase.kt | 5 +- .../cognito/usecases/RememberDeviceUseCase.kt | 42 ++++++ .../auth/cognito/AWSCognitoAuthPluginTest.kt | 16 ++- .../cognito/RealAWSCognitoAuthPluginTest.kt | 119 ---------------- .../AssociateWebAuthnCredentialUseCaseTest.kt | 4 +- .../DeleteWebAuthnCredentialsUseCaseTest.kt | 4 +- .../usecases/FetchDevicesUseCaseTest.kt | 96 +++++++++++++ .../usecases/ForgetDeviceUseCaseTest.kt | 113 +++++++++++++++ .../ListWebAuthnCredentialsUseCaseTest.kt | 4 +- .../usecases/RememberDeviceUseCaseTest.kt | 97 +++++++++++++ 19 files changed, 489 insertions(+), 314 deletions(-) create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/FetchDevicesUseCase.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ForgetDeviceUseCase.kt create mode 100644 aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/RememberDeviceUseCase.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/FetchDevicesUseCaseTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ForgetDeviceUseCaseTest.kt create mode 100644 aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/RememberDeviceUseCaseTest.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 5340c90fa..a277729b8 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 @@ -297,16 +297,16 @@ class AWSCognitoAuthPlugin : AuthPlugin() { enqueue(onSuccess, onError) { queueFacade.fetchAuthSession() } override fun rememberDevice(onSuccess: Action, onError: Consumer) = - enqueue(onSuccess, onError) { queueFacade.rememberDevice() } + enqueue(onSuccess, onError) { useCaseFactory.rememberDevice().execute() } override fun forgetDevice(onSuccess: Action, onError: Consumer) = - enqueue(onSuccess, onError) { queueFacade.forgetDevice() } + enqueue(onSuccess, onError) { useCaseFactory.forgetDevice().execute() } override fun forgetDevice(device: AuthDevice, onSuccess: Action, onError: Consumer) = - enqueue(onSuccess, onError) { queueFacade.forgetDevice(device) } + enqueue(onSuccess, onError) { useCaseFactory.forgetDevice().execute(device) } override fun fetchDevices(onSuccess: Consumer>, onError: Consumer) = - enqueue(onSuccess, onError) { queueFacade.fetchDevices() } + enqueue(onSuccess, onError) { useCaseFactory.fetchDevices().execute() } override fun resetPassword( username: String, diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt index 66d4f081c..1660bec00 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AuthStateMachine.kt @@ -33,6 +33,7 @@ import com.amplifyframework.auth.cognito.actions.SignUpCognitoActions import com.amplifyframework.auth.cognito.actions.UserAuthSignInCognitoActions import com.amplifyframework.auth.cognito.actions.WebAuthnSignInCognitoActions import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.exceptions.SignedOutException import com.amplifyframework.statemachine.Environment import com.amplifyframework.statemachine.StateMachine import com.amplifyframework.statemachine.StateMachineResolver @@ -137,3 +138,12 @@ internal suspend inline fun AuthStateMachine.r ) } } + +// Returns the SignedInState or throws SignedOutException or InvalidStateException +internal suspend fun AuthStateMachine.requireSignedInState(): AuthenticationState.SignedIn { + return when (val state = getCurrentState().authNState) { + is AuthenticationState.SignedIn -> state + is AuthenticationState.SignedOut -> throw SignedOutException() + else -> throw InvalidStateException() + } +} 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 76e63a33d..551b9eb91 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 @@ -18,7 +18,6 @@ package com.amplifyframework.auth.cognito import android.app.Activity import android.content.Intent import com.amplifyframework.auth.AuthCodeDeliveryDetails -import com.amplifyframework.auth.AuthDevice import com.amplifyframework.auth.AuthProvider import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser @@ -259,43 +258,6 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth } } - suspend fun rememberDevice() { - return suspendCoroutine { continuation -> - delegate.rememberDevice( - { continuation.resume(Unit) }, - { continuation.resumeWithException(it) } - ) - } - } - - suspend fun forgetDevice() { - return suspendCoroutine { continuation -> - delegate.forgetDevice( - { continuation.resume(Unit) }, - { continuation.resumeWithException(it) } - ) - } - } - - suspend fun forgetDevice(device: AuthDevice) { - return suspendCoroutine { continuation -> - delegate.forgetDevice( - device, - { continuation.resume(Unit) }, - { continuation.resumeWithException(it) } - ) - } - } - - suspend fun fetchDevices(): List { - return suspendCoroutine { continuation -> - delegate.fetchDevices( - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) - } - } - suspend fun resetPassword( username: String ): AuthResetPasswordResult { 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 7b80e4804..c3a341efc 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 @@ -25,15 +25,11 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.AnalyticsMetadataTy import aws.sdk.kotlin.services.cognitoidentityprovider.model.AttributeType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChallengeNameType import aws.sdk.kotlin.services.cognitoidentityprovider.model.ChangePasswordRequest -import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeviceRememberedStatusType import aws.sdk.kotlin.services.cognitoidentityprovider.model.EmailMfaSettingsType -import aws.sdk.kotlin.services.cognitoidentityprovider.model.ForgetDeviceRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserAttributeVerificationCodeRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserRequest -import aws.sdk.kotlin.services.cognitoidentityprovider.model.ListDevicesRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.SmsMfaSettingsType import aws.sdk.kotlin.services.cognitoidentityprovider.model.SoftwareTokenMfaSettingsType -import aws.sdk.kotlin.services.cognitoidentityprovider.model.UpdateDeviceStatusRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.UpdateUserAttributesRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.UpdateUserAttributesResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareTokenResponseType @@ -48,7 +44,6 @@ import com.amplifyframework.auth.AWSCredentials import com.amplifyframework.auth.AWSTemporaryCredentials import com.amplifyframework.auth.AuthChannelEventName import com.amplifyframework.auth.AuthCodeDeliveryDetails -import com.amplifyframework.auth.AuthDevice import com.amplifyframework.auth.AuthException import com.amplifyframework.auth.AuthFactorType import com.amplifyframework.auth.AuthProvider @@ -1497,135 +1492,6 @@ internal class RealAWSCognitoAuthPlugin( ) } - fun rememberDevice(onSuccess: Action, onError: Consumer) { - authStateMachine.getCurrentState { authState -> - when (val state = authState.authNState) { - is AuthenticationState.SignedIn -> { - GlobalScope.launch { - updateDevice( - authEnvironment.getDeviceMetadata(state.signedInData.username)?.deviceKey, - DeviceRememberedStatusType.Remembered, - onSuccess, - onError - ) - } - } - is AuthenticationState.SignedOut -> { - onError.accept(SignedOutException()) - } - else -> { - onError.accept(InvalidStateException()) - } - } - } - } - - fun forgetDevice(onSuccess: Action, onError: Consumer) { - forgetDevice(AuthDevice.fromId(""), onSuccess, onError) - } - - private fun updateDevice( - alternateDeviceId: String?, - rememberedStatusType: DeviceRememberedStatusType, - onSuccess: Action, - onError: Consumer - ) { - GlobalScope.async { - try { - val tokens = getSession().userPoolTokensResult - authEnvironment.cognitoAuthService.cognitoIdentityProviderClient?.updateDeviceStatus( - UpdateDeviceStatusRequest.invoke { - accessToken = tokens.value?.accessToken - deviceKey = alternateDeviceId - deviceRememberedStatus = rememberedStatusType - } - ) - onSuccess.call() - } catch (e: Exception) { - onError.accept(CognitoAuthExceptionConverter.lookup(e, "Update device ID failed.")) - } - } - } - - fun forgetDevice(device: AuthDevice, onSuccess: Action, onError: Consumer) { - authStateMachine.getCurrentState { authState -> - when (val authNState = authState.authNState) { - is AuthenticationState.SignedIn -> { - GlobalScope.launch { - try { - if (device.id.isEmpty()) { - val deviceKey = authEnvironment.getDeviceMetadata(authNState.signedInData.username) - ?.deviceKey - forgetDevice(deviceKey) - } else { - forgetDevice(device.id) - } - onSuccess.call() - } catch (e: Exception) { - onError.accept(CognitoAuthExceptionConverter.lookup(e, "Failed to forget device.")) - } - } - } - is AuthenticationState.SignedOut -> { - onError.accept(SignedOutException()) - } - else -> { - onError.accept(InvalidStateException()) - } - } - } - } - - private suspend fun forgetDevice(alternateDeviceId: String?) { - val tokens = getSession().userPoolTokensResult - authEnvironment.cognitoAuthService.cognitoIdentityProviderClient?.forgetDevice( - ForgetDeviceRequest.invoke { - accessToken = tokens.value?.accessToken - deviceKey = alternateDeviceId - } - ) - } - - fun fetchDevices(onSuccess: Consumer>, onError: Consumer) { - authStateMachine.getCurrentState { authState -> - when (authState.authNState) { - is AuthenticationState.SignedIn -> { - _fetchDevices(onSuccess, onError) - } - is AuthenticationState.SignedOut -> { - onError.accept(SignedOutException()) - } - else -> { - onError.accept(InvalidStateException()) - } - } - } - } - - private fun _fetchDevices(onSuccess: Consumer>, onError: Consumer) { - GlobalScope.async { - try { - val tokens = getSession().userPoolTokensResult - val response = - authEnvironment.cognitoAuthService.cognitoIdentityProviderClient?.listDevices( - ListDevicesRequest.invoke { - accessToken = tokens.value?.accessToken - } - ) - - val devices = response?.devices?.map { device -> - val id = device.deviceKey ?: "" - val name = device.deviceAttributes?.find { it.name == "device_name" }?.value - AuthDevice.fromId(id, name) - } ?: emptyList() - - onSuccess.accept(devices) - } catch (e: Exception) { - onError.accept(CognitoAuthExceptionConverter.lookup(e, "Fetch devices failed.")) - } - } - } - @OptIn(DelicateCoroutinesApi::class) fun resetPassword( username: String, diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCase.kt index fbcc8f1ef..b8bb5a249 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCase.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCase.kt @@ -22,9 +22,8 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.startWebAuthnRegistration import com.amplifyframework.auth.cognito.AuthStateMachine import com.amplifyframework.auth.cognito.helpers.WebAuthnHelper import com.amplifyframework.auth.cognito.helpers.authLogger -import com.amplifyframework.auth.cognito.requireAuthenticationState +import com.amplifyframework.auth.cognito.requireSignedInState import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions -import com.amplifyframework.statemachine.codegen.states.AuthenticationState.SignedIn import com.amplifyframework.statemachine.util.mask import com.amplifyframework.util.JsonDocument import com.amplifyframework.util.toJsonString @@ -40,7 +39,7 @@ internal class AssociateWebAuthnCredentialUseCase( @Suppress("UNUSED_PARAMETER") suspend fun execute(callingActivity: Activity, options: AuthAssociateWebAuthnCredentialsOptions) { // User must be signed in to call this API - stateMachine.requireAuthenticationState() + stateMachine.requireSignedInState() val accessToken = fetchAuthSession.execute().accessToken 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 2eec0b758..c2d578800 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 @@ -47,4 +47,24 @@ internal class AuthUseCaseFactory( fetchAuthSession = fetchAuthSession(), stateMachine = stateMachine ) + + fun rememberDevice() = RememberDeviceUseCase( + client = authEnvironment.requireIdentityProviderClient(), + fetchAuthSession = fetchAuthSession(), + stateMachine = stateMachine, + environment = authEnvironment + ) + + fun forgetDevice() = ForgetDeviceUseCase( + client = authEnvironment.requireIdentityProviderClient(), + fetchAuthSession = fetchAuthSession(), + stateMachine = stateMachine, + environment = authEnvironment + ) + + fun fetchDevices() = FetchDevicesUseCase( + client = authEnvironment.requireIdentityProviderClient(), + fetchAuthSession = fetchAuthSession(), + stateMachine = stateMachine + ) } diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialUseCase.kt index 97f898bdf..48b3c97e3 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialUseCase.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialUseCase.kt @@ -18,9 +18,8 @@ package com.amplifyframework.auth.cognito.usecases import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient import aws.sdk.kotlin.services.cognitoidentityprovider.deleteWebAuthnCredential import com.amplifyframework.auth.cognito.AuthStateMachine -import com.amplifyframework.auth.cognito.requireAuthenticationState +import com.amplifyframework.auth.cognito.requireSignedInState import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions -import com.amplifyframework.statemachine.codegen.states.AuthenticationState.SignedIn internal class DeleteWebAuthnCredentialUseCase( private val client: CognitoIdentityProviderClient, @@ -30,7 +29,7 @@ internal class DeleteWebAuthnCredentialUseCase( @Suppress("UNUSED_PARAMETER") suspend fun execute(credentialId: String, options: AuthDeleteWebAuthnCredentialOptions) { // User must be signed in to call this API - stateMachine.requireAuthenticationState() + stateMachine.requireSignedInState() val accessToken = fetchAuthSession.execute().accessToken client.deleteWebAuthnCredential { diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/FetchDevicesUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/FetchDevicesUseCase.kt new file mode 100644 index 000000000..656fba296 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/FetchDevicesUseCase.kt @@ -0,0 +1,39 @@ +/* + * 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 aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.listDevices +import com.amplifyframework.auth.AuthDevice +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.requireSignedInState + +internal class FetchDevicesUseCase( + private val client: CognitoIdentityProviderClient, + private val fetchAuthSession: FetchAuthSessionUseCase, + private val stateMachine: AuthStateMachine +) { + suspend fun execute(): List { + stateMachine.requireSignedInState() + val token = fetchAuthSession.execute().accessToken + val response = client.listDevices { accessToken = token } + return response.devices?.map { device -> + val id = device.deviceKey ?: "" + val name = device.deviceAttributes?.find { it.name == "device_name" }?.value + AuthDevice.fromId(id, name) + } ?: emptyList() + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ForgetDeviceUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ForgetDeviceUseCase.kt new file mode 100644 index 000000000..5e7ff260c --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ForgetDeviceUseCase.kt @@ -0,0 +1,44 @@ +/* + * 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 aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.forgetDevice +import com.amplifyframework.auth.AuthDevice +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.requireSignedInState + +internal class ForgetDeviceUseCase( + private val client: CognitoIdentityProviderClient, + private val fetchAuthSession: FetchAuthSessionUseCase, + private val stateMachine: AuthStateMachine, + private val environment: AuthEnvironment +) { + suspend fun execute(device: AuthDevice = AuthDevice.fromId("")) { + val username = stateMachine.requireSignedInState().signedInData.username + val deviceId = when { + device.id.isNotEmpty() -> device.id + else -> environment.getDeviceMetadata(username)?.deviceKey + } + val token = fetchAuthSession.execute().accessToken + + client.forgetDevice { + accessToken = token + deviceKey = deviceId + } + } +} diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCase.kt index c5dea835e..a73ec0476 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCase.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCase.kt @@ -21,11 +21,10 @@ import aws.smithy.kotlin.runtime.time.toJvmInstant import com.amplifyframework.auth.cognito.AuthStateMachine import com.amplifyframework.auth.cognito.options.AWSCognitoAuthListWebAuthnCredentialsOptions.Companion.maxResults import com.amplifyframework.auth.cognito.options.AWSCognitoAuthListWebAuthnCredentialsOptions.Companion.nextToken -import com.amplifyframework.auth.cognito.requireAuthenticationState +import com.amplifyframework.auth.cognito.requireSignedInState import com.amplifyframework.auth.cognito.result.AWSCognitoAuthListWebAuthnCredentialsResult import com.amplifyframework.auth.cognito.result.CognitoWebAuthnCredential import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions -import com.amplifyframework.statemachine.codegen.states.AuthenticationState.SignedIn internal class ListWebAuthnCredentialsUseCase( private val client: CognitoIdentityProviderClient, @@ -34,7 +33,7 @@ internal class ListWebAuthnCredentialsUseCase( ) { suspend fun execute(options: AuthListWebAuthnCredentialsOptions): AWSCognitoAuthListWebAuthnCredentialsResult { // User must be SignedIn to call this API - stateMachine.requireAuthenticationState() + stateMachine.requireSignedInState() val token = fetchAuthSession.execute().accessToken diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/RememberDeviceUseCase.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/RememberDeviceUseCase.kt new file mode 100644 index 000000000..1193fbb81 --- /dev/null +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/RememberDeviceUseCase.kt @@ -0,0 +1,42 @@ +/* + * 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 aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeviceRememberedStatusType +import aws.sdk.kotlin.services.cognitoidentityprovider.updateDeviceStatus +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.cognito.requireSignedInState + +internal class RememberDeviceUseCase( + private val client: CognitoIdentityProviderClient, + private val fetchAuthSession: FetchAuthSessionUseCase, + private val stateMachine: AuthStateMachine, + private val environment: AuthEnvironment +) { + suspend fun execute() { + val username = stateMachine.requireSignedInState().signedInData.username + val deviceId = environment.getDeviceMetadata(username)?.deviceKey + val token = fetchAuthSession.execute().accessToken + + client.updateDeviceStatus { + accessToken = token + deviceKey = deviceId + deviceRememberedStatus = DeviceRememberedStatusType.Remembered + } + } +} 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 5f196bf16..35c4b2663 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 @@ -327,9 +327,11 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Action { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.rememberDevice() + authPlugin.rememberDevice(expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { realPlugin.rememberDevice(any(), any()) } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute() } } @Test @@ -337,9 +339,11 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Action { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.forgetDevice() + authPlugin.forgetDevice(expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { realPlugin.forgetDevice(any(), any()) } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute() } } @Test @@ -348,9 +352,11 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Action { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.forgetDevice() + authPlugin.forgetDevice(expectedDevice, expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { realPlugin.forgetDevice(expectedDevice, any(), any()) } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute(expectedDevice) } } @Test @@ -358,9 +364,11 @@ class AWSCognitoAuthPluginTest { val expectedOnSuccess = Consumer> { } val expectedOnError = Consumer { } + val useCase = authPlugin.useCaseFactory.fetchDevices() + authPlugin.fetchDevices(expectedOnSuccess, expectedOnError) - verify(timeout = CHANNEL_TIMEOUT) { realPlugin.fetchDevices(any(), any()) } + coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute() } } @Test diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt index 342274923..959e600d2 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPluginTest.kt @@ -27,12 +27,9 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.CognitoIdentityProv import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmForgotPasswordRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.ConfirmForgotPasswordResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeliveryMediumType -import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeviceType import aws.sdk.kotlin.services.cognitoidentityprovider.model.EmailMfaSettingsType -import aws.sdk.kotlin.services.cognitoidentityprovider.model.ForgetDeviceResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserAttributeVerificationCodeResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.GetUserResponse -import aws.sdk.kotlin.services.cognitoidentityprovider.model.ListDevicesResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.ResendConfirmationCodeRequest import aws.sdk.kotlin.services.cognitoidentityprovider.model.ResendConfirmationCodeResponse import aws.sdk.kotlin.services.cognitoidentityprovider.model.SetUserMfaPreferenceRequest @@ -46,7 +43,6 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareToken import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifySoftwareTokenResponseType import aws.sdk.kotlin.services.cognitoidentityprovider.model.VerifyUserAttributeResponse import com.amplifyframework.auth.AuthCodeDeliveryDetails -import com.amplifyframework.auth.AuthDevice import com.amplifyframework.auth.AuthException import com.amplifyframework.auth.AuthSession import com.amplifyframework.auth.AuthUser @@ -85,7 +81,6 @@ import com.amplifyframework.core.Consumer import com.amplifyframework.logging.Logger import com.amplifyframework.statemachine.codegen.data.AmplifyCredential import com.amplifyframework.statemachine.codegen.data.CognitoUserPoolTokens -import com.amplifyframework.statemachine.codegen.data.DeviceMetadata import com.amplifyframework.statemachine.codegen.data.SignInMethod import com.amplifyframework.statemachine.codegen.data.SignedInData import com.amplifyframework.statemachine.codegen.data.UserPoolConfiguration @@ -98,7 +93,6 @@ import featureTest.utilities.APICaptorFactory.Companion.onError import io.kotest.assertions.throwables.shouldThrowWithMessage import io.kotest.matchers.booleans.shouldBeTrue import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.every import io.mockk.invoke import io.mockk.mockk @@ -2504,119 +2498,6 @@ class RealAWSCognitoAuthPluginTest { ) } - @Test - fun `forget device invokes ForgetDevice api`() { - val onSuccess = ActionWithLatch() - - coEvery { mockCognitoIPClient.forgetDevice(any()) } answers { ForgetDeviceResponse.invoke {} } - - coEvery { - authEnvironment.getDeviceMetadata("username") - } returns DeviceMetadata.Metadata(deviceKey = "test", deviceGroupKey = "group") - - plugin.forgetDevice(onSuccess, mockk()) - - onSuccess.shouldBeCalled() - coVerify { mockCognitoIPClient.forgetDevice(match { it.deviceKey == "test" }) } - } - - @Test - fun `forget device emits API error`() { - val onError = ConsumerWithLatch() - - coEvery { mockCognitoIPClient.forgetDevice(any()) } throws Exception("failed") - - coEvery { - authEnvironment.getDeviceMetadata("username") - } returns DeviceMetadata.Metadata(deviceKey = "test", deviceGroupKey = "group") - - plugin.forgetDevice(mockk(), onError) - - onError.shouldBeCalled() - assertEquals("failed", onError.captured.cause?.message) - } - - @Test - fun `forget specific device invokes ForgetDevice api`() { - val onSuccess = ActionWithLatch() - - coEvery { mockCognitoIPClient.forgetDevice(any()) } answers { ForgetDeviceResponse.invoke {} } - - plugin.forgetDevice(AuthDevice.fromId("test"), onSuccess, mockk()) - - onSuccess.shouldBeCalled() - coVerify { mockCognitoIPClient.forgetDevice(match { it.deviceKey == "test" }) } - } - - @Test - fun `forget specific device emits API error`() { - val onError = ConsumerWithLatch() - - coEvery { mockCognitoIPClient.forgetDevice(any()) } throws Exception("failed") - - plugin.forgetDevice(AuthDevice.fromId("test"), mockk(), onError) - - onError.shouldBeCalled() - assertEquals("failed", onError.captured.cause?.message) - } - - @Test - fun `fetch devices returns device id and name`() { - val onSuccess = ConsumerWithLatch>() - - coEvery { mockCognitoIPClient.listDevices(any()) } returns ListDevicesResponse.invoke { - devices = listOf( - DeviceType.invoke { - deviceKey = "id1" - deviceAttributes = listOf( - AttributeType.invoke { - name = "device_name" - value = "name1" - } - ) - } - ) - } - - plugin.fetchDevices(onSuccess, mockk()) - - onSuccess.shouldBeCalled() - assertEquals("id1", onSuccess.captured.first().id) - assertEquals("name1", onSuccess.captured.first().name) - } - - @Test - fun `fetch devices returns error if listDevices fails`() { - val onError = ConsumerWithLatch() - coEvery { mockCognitoIPClient.listDevices(any()) } throws Exception("bad") - - plugin.fetchDevices(mockk(), onError) - - onError.shouldBeCalled() - } - - @Test - fun `fetch devices returns error if signed out`() { - val onError = ConsumerWithLatch() - setupCurrentAuthState(authNState = AuthenticationState.SignedOut(mockk())) - - plugin.fetchDevices(mockk(), onError) - - onError.shouldBeCalled() - assertEquals(SignedOutException(), onError.captured) - } - - @Test - fun `fetch devices returns error if not signed in`() { - val onError = ConsumerWithLatch() - setupCurrentAuthState(authNState = AuthenticationState.NotConfigured()) - - plugin.fetchDevices(mockk(), onError) - - onError.shouldBeCalled() - assertEquals(InvalidStateException(), onError.captured) - } - private fun setupCurrentAuthState(authNState: AuthenticationState? = null, authZState: AuthorizationState? = null) { val currentAuthState = mockk { every { this@mockk.authNState } returns authNState diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCaseTest.kt index 1fa152685..4a3596665 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCaseTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/AssociateWebAuthnCredentialUseCaseTest.kt @@ -21,7 +21,7 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.StartWebAuthnRegist import aws.smithy.kotlin.runtime.content.Document import com.amplifyframework.auth.cognito.AuthStateMachine import com.amplifyframework.auth.cognito.helpers.WebAuthnHelper -import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.exceptions.SignedOutException import com.amplifyframework.auth.options.AuthAssociateWebAuthnCredentialsOptions import com.amplifyframework.statemachine.codegen.states.AuthenticationState import io.kotest.assertions.throwables.shouldThrow @@ -60,7 +60,7 @@ class AssociateWebAuthnCredentialUseCaseTest { fun `fails if not in SignedIn state`() = runTest { coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.SignedOut(mockk()) - shouldThrow { + shouldThrow { useCase.execute(mockk(), AuthAssociateWebAuthnCredentialsOptions.defaults()) } } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialsUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialsUseCaseTest.kt index 6fd9cd95d..ad8d8d6e5 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialsUseCaseTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/DeleteWebAuthnCredentialsUseCaseTest.kt @@ -18,7 +18,7 @@ package com.amplifyframework.auth.cognito.usecases import aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeleteWebAuthnCredentialResponse import com.amplifyframework.auth.cognito.AuthStateMachine -import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.exceptions.SignedOutException import com.amplifyframework.auth.options.AuthDeleteWebAuthnCredentialOptions import com.amplifyframework.statemachine.codegen.states.AuthenticationState import io.kotest.assertions.throwables.shouldThrow @@ -49,7 +49,7 @@ class DeleteWebAuthnCredentialsUseCaseTest { fun `fails if not in SignedIn state`() = runTest { coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.SignedOut(mockk()) - shouldThrow { + shouldThrow { useCase.execute("credentialId", AuthDeleteWebAuthnCredentialOptions.defaults()) } } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/FetchDevicesUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/FetchDevicesUseCaseTest.kt new file mode 100644 index 000000000..106c08400 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/FetchDevicesUseCaseTest.kt @@ -0,0 +1,96 @@ +/* + * 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 aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.AttributeType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeviceType +import aws.sdk.kotlin.services.cognitoidentityprovider.model.ListDevicesResponse +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.exceptions.SignedOutException +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.throwables.shouldThrowWithMessage +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class FetchDevicesUseCaseTest { + private val client = mockk() + private val fetchAuthSession: FetchAuthSessionUseCase = mockk { + coEvery { execute().accessToken } returns "access token" + } + private val stateMachine: AuthStateMachine = mockk { + coEvery { getCurrentState().authNState } returns AuthenticationState.SignedIn(mockk(), mockk()) + } + + private val useCase = FetchDevicesUseCase( + client = client, + fetchAuthSession = fetchAuthSession, + stateMachine = stateMachine + ) + + @Test + fun `fetch devices returns device id and name`() = runTest { + coEvery { client.listDevices(any()) } returns ListDevicesResponse { + devices = listOf( + DeviceType { + deviceKey = "id1" + deviceAttributes = listOf( + AttributeType { + name = "device_name" + value = "name1" + } + ) + } + ) + } + + val result = useCase.execute() + result.first().id shouldBe "id1" + result.first().name shouldBe "name1" + } + + @Test + fun `fetch devices returns error if listDevices fails`() = runTest { + coEvery { client.listDevices(any()) } throws Exception("bad") + + shouldThrowWithMessage("bad") { + useCase.execute() + } + } + + @Test + fun `fetch devices returns error if signed out`() = runTest { + coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.SignedOut(mockk()) + + shouldThrow { + useCase.execute() + } + } + + @Test + fun `fetch devices returns error if not signed in`() = runTest { + coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.NotConfigured() + + shouldThrow { + useCase.execute() + } + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ForgetDeviceUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ForgetDeviceUseCaseTest.kt new file mode 100644 index 000000000..14af9cf69 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ForgetDeviceUseCaseTest.kt @@ -0,0 +1,113 @@ +/* + * 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 aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import com.amplifyframework.auth.AuthDevice +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.exceptions.SignedOutException +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.throwables.shouldThrowWithMessage +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ForgetDeviceUseCaseTest { + private val client = mockk(relaxed = true) + private val fetchAuthSession: FetchAuthSessionUseCase = mockk { + coEvery { execute().accessToken } returns "access token" + } + private val stateMachine: AuthStateMachine = mockk { + coEvery { getCurrentState().authNState } returns AuthenticationState.SignedIn( + signedInData = mockk { every { username } returns "user" }, + deviceMetadata = mockk() + ) + } + private val authEnvironment = mockk { + coEvery { getDeviceMetadata("user")?.deviceKey } returns "test deviceKey" + } + + private val useCase = ForgetDeviceUseCase( + client = client, + fetchAuthSession = fetchAuthSession, + stateMachine = stateMachine, + environment = authEnvironment + ) + + @Test + fun `forget device invokes API`() = runTest { + useCase.execute() + + coVerify { + client.forgetDevice( + withArg { request -> + request.accessToken shouldBe "access token" + request.deviceKey shouldBe "test deviceKey" + } + ) + } + } + + @Test + fun `forget device invokes API with device ID`() = runTest { + val device = AuthDevice.fromId("specified id") + + useCase.execute(device) + + coVerify { + client.forgetDevice( + withArg { request -> + request.accessToken shouldBe "access token" + request.deviceKey shouldBe "specified id" + } + ) + } + } + + @Test + fun `forget device returns error if forgetDevice fails`() = runTest { + coEvery { client.forgetDevice(any()) } throws Exception("bad") + + shouldThrowWithMessage("bad") { + useCase.execute() + } + } + + @Test + fun `forget device returns error if signed out`() = runTest { + coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.SignedOut(mockk()) + + shouldThrow { + useCase.execute() + } + } + + @Test + fun `forget device returns error if not signed in`() = runTest { + coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.NotConfigured() + + shouldThrow { + useCase.execute() + } + } +} diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCaseTest.kt index ba08322bf..428aa15ec 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCaseTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/ListWebAuthnCredentialsUseCaseTest.kt @@ -20,7 +20,7 @@ import aws.sdk.kotlin.services.cognitoidentityprovider.model.ListWebAuthnCredent import com.amplifyframework.auth.cognito.AuthStateMachine import com.amplifyframework.auth.cognito.mockWebAuthnCredentialDescription import com.amplifyframework.auth.cognito.options.AWSCognitoAuthListWebAuthnCredentialsOptions -import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.exceptions.SignedOutException import com.amplifyframework.auth.options.AuthListWebAuthnCredentialsOptions import com.amplifyframework.statemachine.codegen.states.AuthenticationState import io.kotest.assertions.throwables.shouldThrow @@ -103,7 +103,7 @@ class ListWebAuthnCredentialsUseCaseTest { fun `fails if not in SignedIn state`() = runTest { coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.SignedOut(mockk()) - shouldThrow { + shouldThrow { useCase.execute(AuthListWebAuthnCredentialsOptions.defaults()) } } diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/RememberDeviceUseCaseTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/RememberDeviceUseCaseTest.kt new file mode 100644 index 000000000..32ca8a593 --- /dev/null +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/usecases/RememberDeviceUseCaseTest.kt @@ -0,0 +1,97 @@ +/* + * 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 aws.sdk.kotlin.services.cognitoidentityprovider.CognitoIdentityProviderClient +import aws.sdk.kotlin.services.cognitoidentityprovider.model.DeviceRememberedStatusType +import com.amplifyframework.auth.cognito.AuthEnvironment +import com.amplifyframework.auth.cognito.AuthStateMachine +import com.amplifyframework.auth.exceptions.InvalidStateException +import com.amplifyframework.auth.exceptions.SignedOutException +import com.amplifyframework.statemachine.codegen.states.AuthenticationState +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.throwables.shouldThrowWithMessage +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RememberDeviceUseCaseTest { + private val client = mockk(relaxed = true) + private val fetchAuthSession: FetchAuthSessionUseCase = mockk { + coEvery { execute().accessToken } returns "access token" + } + private val stateMachine: AuthStateMachine = mockk { + coEvery { getCurrentState().authNState } returns AuthenticationState.SignedIn( + signedInData = mockk { every { username } returns "user" }, + deviceMetadata = mockk() + ) + } + private val authEnvironment = mockk { + coEvery { getDeviceMetadata("user")?.deviceKey } returns "test deviceKey" + } + + private val useCase = RememberDeviceUseCase( + client = client, + fetchAuthSession = fetchAuthSession, + stateMachine = stateMachine, + environment = authEnvironment + ) + + @Test + fun `remember device invokes API`() = runTest { + useCase.execute() + coVerify { + client.updateDeviceStatus( + withArg { request -> + request.accessToken shouldBe "access token" + request.deviceKey shouldBe "test deviceKey" + request.deviceRememberedStatus shouldBe DeviceRememberedStatusType.Remembered + } + ) + } + } + + @Test + fun `remember device returns error if updateDeviceStatus fails`() = runTest { + coEvery { client.updateDeviceStatus(any()) } throws Exception("bad") + + shouldThrowWithMessage("bad") { + useCase.execute() + } + } + + @Test + fun `remember device returns error if signed out`() = runTest { + coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.SignedOut(mockk()) + + shouldThrow { + useCase.execute() + } + } + + @Test + fun `remember device returns error if not signed in`() = runTest { + coEvery { stateMachine.getCurrentState().authNState } returns AuthenticationState.NotConfigured() + + shouldThrow { + useCase.execute() + } + } +}