Skip to content

Commit da6dd6b

Browse files
authored
fix(authenticator): Do not show empty user verification screen (#143)
1 parent 0352f3b commit da6dd6b

File tree

3 files changed

+151
-33
lines changed

3 files changed

+151
-33
lines changed

authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ internal class AuthenticatorViewModel(
408408
}
409409
val verificationAttributeKeys = mechanisms.map { it.toAttributeKey() }
410410
val verificationAttributes = result.data.filter { verificationAttributeKeys.contains(it.key) }
411-
if (hasVerified) {
411+
if (hasVerified || verificationAttributes.isEmpty()) {
412412
handleSignedIn()
413413
} else {
414414
val newState = stateFactory.newVerifyUserState(

authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt

Lines changed: 118 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616
package com.amplifyframework.ui.authenticator
1717

1818
import android.app.Application
19+
import com.amplifyframework.auth.AuthUserAttributeKey.email
20+
import com.amplifyframework.auth.AuthUserAttributeKey.emailVerified
1921
import com.amplifyframework.auth.MFAType
2022
import com.amplifyframework.auth.result.step.AuthSignInStep
23+
import com.amplifyframework.ui.authenticator.auth.VerificationMechanism
2124
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
2225
import com.amplifyframework.ui.authenticator.util.AmplifyResult
26+
import com.amplifyframework.ui.authenticator.util.AmplifyResult.Success
2327
import com.amplifyframework.ui.authenticator.util.AuthConfigurationResult
2428
import com.amplifyframework.ui.authenticator.util.AuthProvider
2529
import com.amplifyframework.ui.testing.CoroutineTestRule
@@ -50,15 +54,16 @@ class AuthenticatorViewModelTest {
5054

5155
@Before
5256
fun setup() {
53-
coEvery { authProvider.getConfiguration() } returns AuthConfigurationResult.Valid(mockk(relaxed = true))
57+
coEvery { authProvider.getConfiguration() } returns mockAmplifyAuthConfiguration()
58+
coEvery { authProvider.getCurrentUser() } returns Success(mockUser())
5459
}
5560

5661
//region start tests
5762

5863
@Test
5964
fun `start only executes once`() = runTest {
60-
viewModel.start(mockAuthConfiguration())
61-
viewModel.start(mockAuthConfiguration())
65+
viewModel.start(mockAuthenticatorConfiguration())
66+
viewModel.start(mockAuthenticatorConfiguration())
6267
advanceUntilIdle()
6368

6469
// fetchAuthSession only called by the first start
@@ -71,7 +76,7 @@ class AuthenticatorViewModelTest {
7176
fun `missing configuration results in an error`() = runTest {
7277
coEvery { authProvider.getConfiguration() } returns AuthConfigurationResult.Missing
7378

74-
viewModel.start(mockAuthConfiguration())
79+
viewModel.start(mockAuthenticatorConfiguration())
7580
advanceUntilIdle()
7681

7782
coVerify(exactly = 0) { authProvider.fetchAuthSession() }
@@ -82,7 +87,7 @@ class AuthenticatorViewModelTest {
8287
fun `invalid configuration results in an error`() = runTest {
8388
coEvery { authProvider.getConfiguration() } returns AuthConfigurationResult.Invalid("Invalid")
8489

85-
viewModel.start(mockAuthConfiguration())
90+
viewModel.start(mockAuthenticatorConfiguration())
8691
advanceUntilIdle()
8792

8893
coVerify(exactly = 0) { authProvider.fetchAuthSession() }
@@ -93,7 +98,7 @@ class AuthenticatorViewModelTest {
9398
fun `fetchAuthSession error during start results in an error`() = runTest {
9499
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Error(mockAuthException())
95100

96-
viewModel.start(mockAuthConfiguration())
101+
viewModel.start(mockAuthenticatorConfiguration())
97102
advanceUntilIdle()
98103

99104
coVerify(exactly = 1) { authProvider.fetchAuthSession() }
@@ -102,10 +107,10 @@ class AuthenticatorViewModelTest {
102107

103108
@Test
104109
fun `getCurrentUser error during start results in an error`() = runTest {
105-
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Success(mockAuthSession(isSignedIn = true))
110+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = true))
106111
coEvery { authProvider.getCurrentUser() } returns AmplifyResult.Error(mockAuthException())
107112

108-
viewModel.start(mockAuthConfiguration())
113+
viewModel.start(mockAuthenticatorConfiguration())
109114
advanceUntilIdle()
110115

111116
coVerify(exactly = 1) {
@@ -117,10 +122,10 @@ class AuthenticatorViewModelTest {
117122

118123
@Test
119124
fun `when already signed in during start the initial state should be signed in`() = runTest {
120-
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Success(mockAuthSession(isSignedIn = true))
121-
coEvery { authProvider.getCurrentUser() } returns AmplifyResult.Success(mockAuthUser())
125+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = true))
126+
coEvery { authProvider.getCurrentUser() } returns Success(mockAuthUser())
122127

123-
viewModel.start(mockAuthConfiguration())
128+
viewModel.start(mockAuthenticatorConfiguration())
124129
advanceUntilIdle()
125130

126131
coVerify(exactly = 1) {
@@ -132,9 +137,9 @@ class AuthenticatorViewModelTest {
132137

133138
@Test
134139
fun `initial step is SignIn`() = runTest {
135-
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Success(mockAuthSession(isSignedIn = false))
140+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
136141

137-
viewModel.start(mockAuthConfiguration(initialStep = AuthenticatorStep.SignIn))
142+
viewModel.start(mockAuthenticatorConfiguration(initialStep = AuthenticatorStep.SignIn))
138143
advanceUntilIdle()
139144

140145
viewModel.currentStep shouldBe AuthenticatorStep.SignIn
@@ -145,97 +150,179 @@ class AuthenticatorViewModelTest {
145150

146151
@Test
147152
fun `TOTPSetup next step shows error if totpSetupDetails is null`() = runTest {
148-
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Success(mockAuthSession(isSignedIn = false))
149-
coEvery { authProvider.signIn(any(), any()) } returns AmplifyResult.Success(
153+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
154+
coEvery { authProvider.signIn(any(), any()) } returns Success(
150155
mockSignInResult(
151156
signInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP,
152157
totpSetupDetails = null
153158
)
154159
)
155160

156-
viewModel.start(mockAuthConfiguration(initialStep = AuthenticatorStep.SignIn))
161+
viewModel.start(mockAuthenticatorConfiguration(initialStep = AuthenticatorStep.SignIn))
157162

158163
viewModel.signIn("username", "password")
159164
viewModel.currentStep shouldBe AuthenticatorStep.Error
160165
}
161166

162167
@Test
163168
fun `TOTPSetup next step shows SignInContinueWithTotpSetup screen`() = runTest {
164-
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Success(mockAuthSession(isSignedIn = false))
165-
coEvery { authProvider.signIn(any(), any()) } returns AmplifyResult.Success(
169+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
170+
coEvery { authProvider.signIn(any(), any()) } returns Success(
166171
mockSignInResult(
167172
signInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP,
168173
totpSetupDetails = mockk(relaxed = true)
169174
)
170175
)
171176

172-
viewModel.start(mockAuthConfiguration(initialStep = AuthenticatorStep.SignIn))
177+
viewModel.start(mockAuthenticatorConfiguration(initialStep = AuthenticatorStep.SignIn))
173178

174179
viewModel.signIn("username", "password")
175180
viewModel.currentStep shouldBe AuthenticatorStep.SignInContinueWithTotpSetup
176181
}
177182

178183
@Test
179184
fun `TOTP Code next step shows the SignInConfirmTotpCode screen`() = runTest {
180-
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Success(mockAuthSession(isSignedIn = false))
181-
coEvery { authProvider.signIn(any(), any()) } returns AmplifyResult.Success(
185+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
186+
coEvery { authProvider.signIn(any(), any()) } returns Success(
182187
mockSignInResult(signInStep = AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE)
183188
)
184189

185-
viewModel.start(mockAuthConfiguration(initialStep = AuthenticatorStep.SignIn))
190+
viewModel.start(mockAuthenticatorConfiguration(initialStep = AuthenticatorStep.SignIn))
186191

187192
viewModel.signIn("username", "password")
188193
viewModel.currentStep shouldBe AuthenticatorStep.SignInConfirmTotpCode
189194
}
190195

191196
@Test
192197
fun `MFA selection next step shows error if allowedMFATypes is null`() = runTest {
193-
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Success(mockAuthSession(isSignedIn = false))
194-
coEvery { authProvider.signIn(any(), any()) } returns AmplifyResult.Success(
198+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
199+
coEvery { authProvider.signIn(any(), any()) } returns Success(
195200
mockSignInResult(
196201
signInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION,
197202
allowedMFATypes = null
198203
)
199204
)
200205

201-
viewModel.start(mockAuthConfiguration(initialStep = AuthenticatorStep.SignIn))
206+
viewModel.start(mockAuthenticatorConfiguration(initialStep = AuthenticatorStep.SignIn))
202207

203208
viewModel.signIn("username", "password")
204209
viewModel.currentStep shouldBe AuthenticatorStep.Error
205210
}
206211

207212
@Test
208213
fun `MFA selection next step shows error if allowedMFATypes is empty`() = runTest {
209-
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Success(mockAuthSession(isSignedIn = false))
210-
coEvery { authProvider.signIn(any(), any()) } returns AmplifyResult.Success(
214+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
215+
coEvery { authProvider.signIn(any(), any()) } returns Success(
211216
mockSignInResult(
212217
signInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION,
213218
allowedMFATypes = emptySet()
214219
)
215220
)
216221

217-
viewModel.start(mockAuthConfiguration(initialStep = AuthenticatorStep.SignIn))
222+
viewModel.start(mockAuthenticatorConfiguration(initialStep = AuthenticatorStep.SignIn))
218223

219224
viewModel.signIn("username", "password")
220225
viewModel.currentStep shouldBe AuthenticatorStep.Error
221226
}
222227

223228
@Test
224229
fun `MFA Selection next step shows the SignInContinueWithMfaSelection screen`() = runTest {
225-
coEvery { authProvider.fetchAuthSession() } returns AmplifyResult.Success(mockAuthSession(isSignedIn = false))
226-
coEvery { authProvider.signIn(any(), any()) } returns AmplifyResult.Success(
230+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
231+
coEvery { authProvider.signIn(any(), any()) } returns Success(
227232
mockSignInResult(
228233
signInStep = AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION,
229234
allowedMFATypes = setOf(MFAType.TOTP, MFAType.SMS)
230235
)
231236
)
232237

233-
viewModel.start(mockAuthConfiguration(initialStep = AuthenticatorStep.SignIn))
238+
viewModel.start(mockAuthenticatorConfiguration(initialStep = AuthenticatorStep.SignIn))
234239

235240
viewModel.signIn("username", "password")
236241
viewModel.currentStep shouldBe AuthenticatorStep.SignInContinueWithMfaSelection
237242
}
238243

244+
@Test
245+
fun `user attribute verification screen is shown if user has no verified attributes`() = runTest {
246+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
247+
coEvery { authProvider.signIn(any(), any()) } returns Success(mockSignInResult())
248+
coEvery { authProvider.getConfiguration() } returns mockAmplifyAuthConfiguration(
249+
verificationMechanisms = setOf(VerificationMechanism.Email)
250+
)
251+
coEvery { authProvider.fetchUserAttributes() } returns Success(
252+
mockUserAttributes(email() to "email", emailVerified() to "false")
253+
)
254+
255+
viewModel.start(mockAuthenticatorConfiguration())
256+
viewModel.signIn("username", "password")
257+
258+
viewModel.currentStep shouldBe AuthenticatorStep.VerifyUser
259+
}
260+
261+
@Test
262+
fun `user attribute verification screen is not shown if user has verified attributes`() = runTest {
263+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
264+
coEvery { authProvider.signIn(any(), any()) } returns Success(mockSignInResult())
265+
coEvery { authProvider.getConfiguration() } returns mockAmplifyAuthConfiguration(
266+
verificationMechanisms = setOf(VerificationMechanism.Email)
267+
)
268+
coEvery { authProvider.fetchUserAttributes() } returns Success(
269+
mockUserAttributes(email() to "email", emailVerified() to "true") // email is already verified
270+
)
271+
272+
viewModel.start(mockAuthenticatorConfiguration())
273+
viewModel.signIn("username", "password")
274+
275+
viewModel.currentStep shouldBe AuthenticatorStep.SignedIn
276+
}
277+
278+
@Test
279+
fun `user attribute verification screen is not shown if there are no verification mechanisms`() = runTest {
280+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
281+
coEvery { authProvider.signIn(any(), any()) } returns Success(mockSignInResult())
282+
coEvery { authProvider.getConfiguration() } returns mockAmplifyAuthConfiguration(
283+
verificationMechanisms = emptySet() // no verification mechanisms
284+
)
285+
coEvery { authProvider.fetchUserAttributes() } returns Success(
286+
mockUserAttributes(email() to "email", emailVerified() to "false")
287+
)
288+
289+
viewModel.start(mockAuthenticatorConfiguration())
290+
viewModel.signIn("username", "password")
291+
292+
viewModel.currentStep shouldBe AuthenticatorStep.SignedIn
293+
}
294+
295+
@Test
296+
fun `user attribute verification screen is not shown if cannot fetch user attributes`() = runTest {
297+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
298+
coEvery { authProvider.signIn(any(), any()) } returns Success(mockSignInResult())
299+
coEvery { authProvider.getConfiguration() } returns mockAmplifyAuthConfiguration(
300+
verificationMechanisms = setOf(VerificationMechanism.Email)
301+
)
302+
// cannot fetch user attributes
303+
coEvery { authProvider.fetchUserAttributes() } returns AmplifyResult.Error(mockk(relaxed = true))
304+
305+
viewModel.start(mockAuthenticatorConfiguration())
306+
viewModel.signIn("username", "password")
307+
308+
viewModel.currentStep shouldBe AuthenticatorStep.SignedIn
309+
}
310+
311+
@Test
312+
fun `user attribute verification screen is not shown if user does not have the required attributes`() = runTest {
313+
coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false))
314+
coEvery { authProvider.signIn(any(), any()) } returns Success(mockSignInResult())
315+
coEvery { authProvider.getConfiguration() } returns mockAmplifyAuthConfiguration(
316+
verificationMechanisms = setOf(VerificationMechanism.Email)
317+
)
318+
coEvery { authProvider.fetchUserAttributes() } returns Success(mockUserAttributes()) // no email attribute
319+
320+
viewModel.start(mockAuthenticatorConfiguration())
321+
viewModel.signIn("username", "password")
322+
323+
viewModel.currentStep shouldBe AuthenticatorStep.SignedIn
324+
}
325+
239326
//endregion
240327
//region helpers
241328
private val AuthenticatorViewModel.currentStep: AuthenticatorStep

authenticator/src/test/java/com/amplifyframework/ui/authenticator/MockAuthenticatorData.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,25 @@ import com.amplifyframework.auth.AuthCodeDeliveryDetails
1919
import com.amplifyframework.auth.AuthException
2020
import com.amplifyframework.auth.AuthSession
2121
import com.amplifyframework.auth.AuthUser
22+
import com.amplifyframework.auth.AuthUserAttribute
23+
import com.amplifyframework.auth.AuthUserAttributeKey
2224
import com.amplifyframework.auth.MFAType
2325
import com.amplifyframework.auth.TOTPSetupDetails
2426
import com.amplifyframework.auth.result.AuthSignInResult
2527
import com.amplifyframework.auth.result.step.AuthNextSignInStep
2628
import com.amplifyframework.auth.result.step.AuthSignInStep
29+
import com.amplifyframework.ui.authenticator.auth.AmplifyAuthConfiguration
30+
import com.amplifyframework.ui.authenticator.auth.PasswordCriteria
31+
import com.amplifyframework.ui.authenticator.auth.SignInMethod
32+
import com.amplifyframework.ui.authenticator.auth.VerificationMechanism
2733
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
2834
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
2935
import com.amplifyframework.ui.authenticator.forms.SignUpFormBuilder
3036
import com.amplifyframework.ui.authenticator.options.TotpOptions
37+
import com.amplifyframework.ui.authenticator.util.AuthConfigurationResult
38+
import io.mockk.mockk
3139

32-
internal fun mockAuthConfiguration(
40+
internal fun mockAuthenticatorConfiguration(
3341
initialStep: AuthenticatorInitialStep = AuthenticatorStep.SignIn,
3442
signUpForm: SignUpFormBuilder.() -> Unit = {},
3543
totpOptions: TotpOptions? = null
@@ -39,6 +47,20 @@ internal fun mockAuthConfiguration(
3947
totpOptions = totpOptions
4048
)
4149

50+
internal fun mockAmplifyAuthConfiguration(
51+
signInMethod: SignInMethod = SignInMethod.Username,
52+
signUpAttributes: List<AuthUserAttributeKey> = emptyList(),
53+
passwordCriteria: PasswordCriteria = mockk(relaxed = true),
54+
verificationMechanisms: Set<VerificationMechanism> = emptySet()
55+
) = AuthConfigurationResult.Valid(
56+
AmplifyAuthConfiguration(
57+
signInMethod = signInMethod,
58+
signUpAttributes = signUpAttributes,
59+
passwordCriteria = passwordCriteria,
60+
verificationMechanisms = verificationMechanisms
61+
)
62+
)
63+
4264
internal fun mockAuthException(
4365
message: String = "A test exception",
4466
recoverySuggestion: String = "A test suggestion",
@@ -87,3 +109,12 @@ internal fun mockNextSignInStep(
87109
totpSetupDetails: TOTPSetupDetails? = null,
88110
allowedMFATypes: Set<MFAType>? = null
89111
) = AuthNextSignInStep(signInStep, additionalInfo, codeDeliveryDetails, totpSetupDetails, allowedMFATypes)
112+
113+
internal fun mockUserAttributes(
114+
vararg attribute: Pair<AuthUserAttributeKey, String>
115+
) = attribute.map { AuthUserAttribute(it.first, it.second) }
116+
117+
internal fun mockUser(
118+
userId: String = "userId",
119+
username: String = "username"
120+
) = AuthUser(userId, username)

0 commit comments

Comments
 (0)