Skip to content

Commit 179a95f

Browse files
authored
chore(auth): Move deleteUser to use case (#3015)
1 parent 71d9031 commit 179a95f

File tree

11 files changed

+361
-185
lines changed

11 files changed

+361
-185
lines changed

aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPlugin.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ class AWSCognitoAuthPlugin : AuthPlugin<AWSCognitoAuthService>() {
412412
) { queueFacade.signOut(options) }
413413

414414
override fun deleteUser(onSuccess: Action, onError: Consumer<AuthException>) = enqueue(onSuccess, onError) {
415-
queueFacade.deleteUser()
415+
useCaseFactory.deleteUser().execute()
416416
}
417417

418418
override fun setUpTOTP(onSuccess: Consumer<TOTPSetupDetails>, onError: Consumer<AuthException>) =

aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/KotlinAuthFacadeInternal.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,6 @@ internal class KotlinAuthFacadeInternal(private val delegate: RealAWSCognitoAuth
141141
delegate.signOut(options) { continuation.resume(it) }
142142
}
143143

144-
suspend fun deleteUser() = suspendCoroutine { continuation ->
145-
delegate.deleteUser(
146-
{ continuation.resume(Unit) },
147-
{ continuation.resumeWithException(it) }
148-
)
149-
}
150-
151144
suspend fun federateToIdentityPool(
152145
providerToken: String,
153146
authProvider: AuthProvider,

aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/RealAWSCognitoAuthPlugin.kt

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ import com.amplifyframework.statemachine.codegen.errors.SessionError
9393
import com.amplifyframework.statemachine.codegen.events.AuthEvent
9494
import com.amplifyframework.statemachine.codegen.events.AuthenticationEvent
9595
import com.amplifyframework.statemachine.codegen.events.AuthorizationEvent
96-
import com.amplifyframework.statemachine.codegen.events.DeleteUserEvent
9796
import com.amplifyframework.statemachine.codegen.events.HostedUIEvent
9897
import com.amplifyframework.statemachine.codegen.events.SetupTOTPEvent
9998
import com.amplifyframework.statemachine.codegen.events.SignInChallengeEvent
@@ -102,7 +101,6 @@ import com.amplifyframework.statemachine.codegen.events.SignOutEvent
102101
import com.amplifyframework.statemachine.codegen.states.AuthState
103102
import com.amplifyframework.statemachine.codegen.states.AuthenticationState
104103
import com.amplifyframework.statemachine.codegen.states.AuthorizationState
105-
import com.amplifyframework.statemachine.codegen.states.DeleteUserState
106104
import com.amplifyframework.statemachine.codegen.states.HostedUISignInState
107105
import com.amplifyframework.statemachine.codegen.states.SRPSignInState
108106
import com.amplifyframework.statemachine.codegen.states.SetupTOTPState
@@ -117,10 +115,8 @@ import java.util.concurrent.atomic.AtomicReference
117115
import kotlin.coroutines.resume
118116
import kotlin.coroutines.resumeWithException
119117
import kotlin.coroutines.suspendCoroutine
120-
import kotlinx.coroutines.GlobalScope
121118
import kotlinx.coroutines.flow.collect
122119
import kotlinx.coroutines.flow.takeWhile
123-
import kotlinx.coroutines.launch
124120

125121
internal class RealAWSCognitoAuthPlugin(
126122
val configuration: AuthConfiguration,
@@ -1238,68 +1234,6 @@ internal class RealAWSCognitoAuthPlugin(
12381234
)
12391235
}
12401236

1241-
fun deleteUser(onSuccess: Action, onError: Consumer<AuthException>) {
1242-
authStateMachine.getCurrentState { authState ->
1243-
when (authState.authNState) {
1244-
is AuthenticationState.SignedIn -> {
1245-
GlobalScope.launch {
1246-
try {
1247-
val accessToken = getSession().userPoolTokensResult.value?.accessToken
1248-
accessToken?.let {
1249-
_deleteUser(accessToken, onSuccess, onError)
1250-
} ?: onError.accept(SignedOutException())
1251-
} catch (error: Exception) {
1252-
onError.accept(SignedOutException())
1253-
}
1254-
}
1255-
}
1256-
is AuthenticationState.SignedOut -> onError.accept(SignedOutException())
1257-
else -> onError.accept(InvalidStateException())
1258-
}
1259-
}
1260-
}
1261-
1262-
private fun _deleteUser(token: String, onSuccess: Action, onError: Consumer<AuthException>) {
1263-
val listenerToken = StateChangeListenerToken()
1264-
var deleteUserException: Exception? = null
1265-
authStateMachine.listen(
1266-
listenerToken,
1267-
{ authState ->
1268-
if (authState is AuthState.Configured) {
1269-
val (authNState, authZState) = authState
1270-
val exception = deleteUserException
1271-
when {
1272-
authZState is AuthorizationState.DeletingUser &&
1273-
authZState.deleteUserState is DeleteUserState.Error -> {
1274-
deleteUserException = authZState.deleteUserState.exception
1275-
}
1276-
authNState is AuthenticationState.SignedOut && authZState is AuthorizationState.Configured -> {
1277-
sendHubEvent(AuthChannelEventName.USER_DELETED.toString())
1278-
authStateMachine.cancel(listenerToken)
1279-
onSuccess.call()
1280-
}
1281-
authZState is AuthorizationState.SessionEstablished && exception != null -> {
1282-
authStateMachine.cancel(listenerToken)
1283-
onError.accept(
1284-
CognitoAuthExceptionConverter.lookup(
1285-
exception,
1286-
"Request to delete user may have failed. Please check exception stack"
1287-
)
1288-
)
1289-
}
1290-
else -> {
1291-
// No - op
1292-
}
1293-
}
1294-
}
1295-
},
1296-
{
1297-
val event = DeleteUserEvent(DeleteUserEvent.EventType.DeleteUser(accessToken = token))
1298-
authStateMachine.send(event)
1299-
}
1300-
)
1301-
}
1302-
13031237
private fun addAuthStateChangeListener() {
13041238
authStateMachine.listen(
13051239
StateChangeListenerToken(),

aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/usecases/AuthUseCaseFactory.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,9 @@ internal class AuthUseCaseFactory(
159159
fetchMfaPreference = fetchMfaPreference(),
160160
stateMachine = stateMachine
161161
)
162+
163+
fun deleteUser() = DeleteUserUseCase(
164+
fetchAuthSession = fetchAuthSession(),
165+
stateMachine = stateMachine
166+
)
162167
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.auth.cognito.usecases
17+
18+
import com.amplifyframework.auth.AuthChannelEventName
19+
import com.amplifyframework.auth.cognito.AuthStateMachine
20+
import com.amplifyframework.auth.cognito.CognitoAuthExceptionConverter
21+
import com.amplifyframework.auth.cognito.helpers.collectWhile
22+
import com.amplifyframework.auth.cognito.requireAccessToken
23+
import com.amplifyframework.auth.cognito.requireSignedInState
24+
import com.amplifyframework.auth.plugins.core.AuthHubEventEmitter
25+
import com.amplifyframework.statemachine.codegen.events.DeleteUserEvent
26+
import com.amplifyframework.statemachine.codegen.states.AuthenticationState
27+
import com.amplifyframework.statemachine.codegen.states.AuthorizationState
28+
import com.amplifyframework.statemachine.codegen.states.DeleteUserState
29+
import kotlinx.coroutines.flow.onStart
30+
31+
internal class DeleteUserUseCase(
32+
private val fetchAuthSession: FetchAuthSessionUseCase,
33+
private val stateMachine: AuthStateMachine,
34+
private val emitter: AuthHubEventEmitter = AuthHubEventEmitter()
35+
) {
36+
37+
suspend fun execute() {
38+
stateMachine.requireSignedInState()
39+
40+
val token = fetchAuthSession.execute().requireAccessToken()
41+
42+
var deleteUserException: Exception? = null
43+
stateMachine.state
44+
.onStart {
45+
val event = DeleteUserEvent(DeleteUserEvent.EventType.DeleteUser(accessToken = token))
46+
stateMachine.send(event)
47+
}.collectWhile { authState ->
48+
val authNState = authState.authNState
49+
val authZState = authState.authZState
50+
51+
when {
52+
authZState is AuthorizationState.DeletingUser &&
53+
authZState.deleteUserState is DeleteUserState.Error -> {
54+
// Hold on to the exception until the state machine settles
55+
deleteUserException = authZState.deleteUserState.exception
56+
true
57+
}
58+
authNState is AuthenticationState.SignedOut && authZState is AuthorizationState.Configured -> {
59+
emitter.sendHubEvent(AuthChannelEventName.USER_DELETED.toString())
60+
false // done
61+
}
62+
authZState is AuthorizationState.SessionEstablished && deleteUserException != null -> {
63+
throw CognitoAuthExceptionConverter.lookup(
64+
deleteUserException!!,
65+
"Request to delete user may have failed. Please check exception stack"
66+
)
67+
}
68+
else -> true // no-op
69+
}
70+
}
71+
}
72+
}

aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AWSCognitoAuthPluginTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,9 +640,10 @@ class AWSCognitoAuthPluginTest {
640640
val expectedOnSuccess = Action { }
641641
val expectedOnError = Consumer<AuthException> { }
642642

643+
val useCase = authPlugin.useCaseFactory.deleteUser()
643644
authPlugin.deleteUser(expectedOnSuccess, expectedOnError)
644645

645-
verify(timeout = CHANNEL_TIMEOUT) { realPlugin.deleteUser(any(), any()) }
646+
coVerify(timeout = CHANNEL_TIMEOUT) { useCase.execute() }
646647
}
647648

648649
@Test

aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/AuthConfigurationTest.kt

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@ import com.amplifyframework.auth.AuthUserAttributeKey
1919
import com.amplifyframework.auth.cognito.options.AuthFlowType
2020
import com.amplifyframework.auth.exceptions.ConfigurationException
2121
import com.amplifyframework.core.configuration.AmplifyOutputsData
22+
import com.amplifyframework.statemachine.codegen.data.UserPoolConfiguration
2223
import com.amplifyframework.testutils.configuration.amplifyOutputsData
2324
import io.kotest.assertions.throwables.shouldThrow
25+
import io.kotest.assertions.throwables.shouldThrowWithMessage
2426
import io.kotest.matchers.booleans.shouldBeFalse
2527
import io.kotest.matchers.booleans.shouldBeTrue
2628
import io.kotest.matchers.collections.shouldBeEmpty
2729
import io.kotest.matchers.collections.shouldContainExactly
2830
import io.kotest.matchers.nulls.shouldBeNull
2931
import io.kotest.matchers.nulls.shouldNotBeNull
3032
import io.kotest.matchers.shouldBe
33+
import kotlin.test.assertEquals
3134
import org.json.JSONObject
3235
import org.junit.Test
3336

@@ -270,6 +273,102 @@ class AuthConfigurationTest {
270273
}
271274
}
272275

276+
@Test
277+
fun `custom endpoint with query fails`() {
278+
val configJsonObject = JSONObject()
279+
configJsonObject.put("PoolId", "TestUserPool")
280+
configJsonObject.put("AppClientId", "0000000000")
281+
configJsonObject.put("Region", "test-region")
282+
val invalidEndpoint = "fsjjdh.com?q=id"
283+
configJsonObject.put("Endpoint", invalidEndpoint)
284+
val expectedErrorMessage = "Invalid endpoint value $invalidEndpoint. Expected fully qualified hostname with " +
285+
"no scheme, no path and no query"
286+
287+
shouldThrowWithMessage<Exception>(expectedErrorMessage) {
288+
UserPoolConfiguration.fromJson(configJsonObject).build()
289+
}
290+
}
291+
292+
@Test
293+
fun `custom endpoint with path fails`() {
294+
val configJsonObject = JSONObject()
295+
configJsonObject.put("PoolId", "TestUserPool")
296+
configJsonObject.put("AppClientId", "0000000000")
297+
configJsonObject.put("Region", "test-region")
298+
val invalidEndpoint = "fsjjdh.com/id"
299+
configJsonObject.put("Endpoint", invalidEndpoint)
300+
val expectedErrorMessage = "Invalid endpoint value $invalidEndpoint. Expected fully qualified hostname with " +
301+
"no scheme, no path and no query"
302+
303+
shouldThrowWithMessage<Exception>(expectedErrorMessage) {
304+
UserPoolConfiguration.fromJson(configJsonObject).build()
305+
}
306+
}
307+
308+
@Test
309+
fun `custom endpoint with scheme fails`() {
310+
val configJsonObject = JSONObject()
311+
configJsonObject.put("PoolId", "TestUserPool")
312+
configJsonObject.put("AppClientId", "0000000000")
313+
configJsonObject.put("Region", "test-region")
314+
315+
val invalidEndpoint = "https://fsjjdh.com"
316+
configJsonObject.put("Endpoint", invalidEndpoint)
317+
val expectedErrorMessage = "Invalid endpoint value $invalidEndpoint. Expected fully qualified hostname with " +
318+
"no scheme, no path and no query"
319+
320+
shouldThrowWithMessage<Exception>(expectedErrorMessage) {
321+
UserPoolConfiguration.fromJson(configJsonObject).build()
322+
}
323+
}
324+
325+
@Test
326+
fun `custom endpoint with no query,path, scheme success`() {
327+
val configJsonObject = JSONObject()
328+
val poolId = "TestUserPool"
329+
val region = "test-region"
330+
val appClientId = "0000000000"
331+
val endpoint = "fsjjdh.com"
332+
configJsonObject.put("PoolId", poolId)
333+
configJsonObject.put("AppClientId", appClientId)
334+
configJsonObject.put("Region", region)
335+
configJsonObject.put("Endpoint", endpoint)
336+
337+
val userPool = UserPoolConfiguration.fromJson(configJsonObject).build()
338+
assertEquals(userPool.region, region, "Regions do not match expected")
339+
assertEquals(userPool.poolId, poolId, "Pool id do not match expected")
340+
assertEquals(userPool.appClient, appClientId, "AppClientId do not match expected")
341+
assertEquals(userPool.endpoint, "https://$endpoint", "Endpoint do not match expected")
342+
}
343+
344+
@Test
345+
fun `validate auth flow type defaults to user_srp_auth for invalid types`() {
346+
val configJsonObject = JSONObject()
347+
val configAuthJsonObject = JSONObject()
348+
val configAuthDefaultJsonObject = JSONObject()
349+
configAuthDefaultJsonObject.put("authenticationFlowType", "INVALID_FLOW_TYPE")
350+
configAuthJsonObject.put("Default", configAuthDefaultJsonObject)
351+
configJsonObject.put("Auth", configAuthJsonObject)
352+
val configuration = AuthConfiguration.fromJson(configJsonObject)
353+
assertEquals(configuration.authFlowType, AuthFlowType.USER_SRP_AUTH, "Auth flow types do not match expected")
354+
}
355+
356+
@Test
357+
fun `validate auth flow type success`() {
358+
val configJsonObject = JSONObject()
359+
val configAuthJsonObject = JSONObject()
360+
val configAuthDefaultJsonObject = JSONObject()
361+
configAuthDefaultJsonObject.put("authenticationFlowType", "USER_PASSWORD_AUTH")
362+
configAuthJsonObject.put("Default", configAuthDefaultJsonObject)
363+
configJsonObject.put("Auth", configAuthJsonObject)
364+
val configuration = AuthConfiguration.fromJson(configJsonObject)
365+
assertEquals(
366+
configuration.authFlowType,
367+
AuthFlowType.USER_PASSWORD_AUTH,
368+
"Auth flow types do not match expected"
369+
)
370+
}
371+
273372
private fun getAuthConfig() = jsonObject.getJSONObject("Auth").getJSONObject("Default")
274373
private fun getPasswordSettings() = jsonObject.getJSONObject("Auth").getJSONObject("Default")
275374
.getJSONObject("passwordProtectionSettings")

0 commit comments

Comments
 (0)