Skip to content

Commit 03b6306

Browse files
committed
feat: e2e tests for google sign in
1 parent 16cda47 commit 03b6306

File tree

11 files changed

+660
-15
lines changed

11 files changed

+660
-15
lines changed

auth/src/main/java/com/firebase/ui/auth/compose/FirebaseAuthUI.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ package com.firebase.ui.auth.compose
1616

1717
import android.content.Context
1818
import androidx.annotation.RestrictTo
19-
import com.firebase.ui.auth.compose.configuration.auth_provider.signOutFromGoogle
2019
import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
20+
import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider
21+
import com.firebase.ui.auth.compose.configuration.auth_provider.signOutFromGoogle
2122
import com.google.firebase.FirebaseApp
2223
import com.google.firebase.auth.FirebaseAuth
2324
import com.google.firebase.auth.FirebaseAuth.AuthStateListener
@@ -72,6 +73,9 @@ class FirebaseAuthUI private constructor(
7273

7374
private val _authStateFlow = MutableStateFlow<AuthState>(AuthState.Idle)
7475

76+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
77+
var testCredentialManagerProvider: AuthProvider.Google.CredentialManagerProvider? = null
78+
7579
/**
7680
* Checks whether a user is currently signed in.
7781
*

auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/AuthProvider.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import android.app.Activity
1818
import android.content.Context
1919
import android.net.Uri
2020
import android.util.Log
21+
import androidx.annotation.RestrictTo
2122
import androidx.compose.ui.graphics.Color
2223
import androidx.core.net.toUri
2324
import androidx.credentials.CredentialManager
@@ -536,7 +537,8 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
536537
* Result container for Google Sign-In credential flow.
537538
* @suppress
538539
*/
539-
internal data class GoogleSignInResult(
540+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
541+
data class GoogleSignInResult(
540542
val credential: AuthCredential,
541543
val displayName: String?,
542544
val photoUrl: Uri?
@@ -570,9 +572,11 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
570572
* An interface to wrap the Credential Manager flow for Google Sign-In.
571573
* @suppress
572574
*/
573-
internal interface CredentialManagerProvider {
575+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
576+
interface CredentialManagerProvider {
574577
suspend fun getGoogleCredential(
575578
context: Context,
579+
credentialManager: CredentialManager,
576580
serverClientId: String,
577581
filterByAuthorizedAccounts: Boolean,
578582
autoSelectEnabled: Boolean
@@ -583,14 +587,15 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
583587
* The default implementation of [CredentialManagerProvider].
584588
* @suppress
585589
*/
586-
internal class DefaultCredentialManagerProvider : CredentialManagerProvider {
590+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
591+
class DefaultCredentialManagerProvider : CredentialManagerProvider {
587592
override suspend fun getGoogleCredential(
588593
context: Context,
594+
credentialManager: CredentialManager,
589595
serverClientId: String,
590596
filterByAuthorizedAccounts: Boolean,
591-
autoSelectEnabled: Boolean
597+
autoSelectEnabled: Boolean,
592598
): GoogleSignInResult {
593-
val credentialManager = CredentialManager.create(context)
594599
val googleIdOption = GetGoogleIdOption.Builder()
595600
.setServerClientId(serverClientId)
596601
.setFilterByAuthorizedAccounts(filterByAuthorizedAccounts)

auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ internal suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword(
166166
val accountLinkingException = AuthException.AccountLinkingRequiredException(
167167
message = "An account already exists with this email. " +
168168
"Please sign in with your existing account.",
169-
email = e.email,
169+
email = e.email ?: email,
170170
credential = if (canUpgrade) {
171171
e.updatedCredential ?: pendingCredential
172172
} else {

auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/GoogleAuthProvider+FirebaseAuthUI.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,14 @@ internal suspend fun FirebaseAuthUI.signInWithGoogle(
130130
}
131131
}
132132

133-
val result = credentialManagerProvider.getGoogleCredential(
134-
context = context,
135-
serverClientId = provider.serverClientId!!,
136-
filterByAuthorizedAccounts = true,
137-
autoSelectEnabled = false
138-
)
133+
val result =
134+
(testCredentialManagerProvider ?: credentialManagerProvider).getGoogleCredential(
135+
context = context,
136+
credentialManager = CredentialManager.create(context),
137+
serverClientId = provider.serverClientId!!,
138+
filterByAuthorizedAccounts = true,
139+
autoSelectEnabled = false
140+
)
139141

140142
signInAndLinkWithCredential(
141143
config = config,

auth/src/main/java/com/firebase/ui/auth/compose/ui/screens/email/EmailAuthScreen.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,9 @@ fun EmailAuthScreen(
263263
}
264264
)
265265

266-
if (isErrorDialogVisible.value) {
266+
if (isErrorDialogVisible.value &&
267+
(authState as AuthState.Error).exception !is AuthException.AccountLinkingRequiredException
268+
) {
267269
ErrorRecoveryDialog(
268270
error = when ((authState as AuthState.Error).exception) {
269271
is AuthException -> (authState as AuthState.Error).exception as AuthException

auth/src/test/java/com/firebase/ui/auth/compose/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package com.firebase.ui.auth.compose.configuration.auth_provider
1717
import android.content.Context
1818
import android.net.Uri
1919
import androidx.core.net.toUri
20+
import androidx.credentials.CredentialManager
2021
import androidx.test.core.app.ApplicationProvider
2122
import com.firebase.ui.auth.compose.AuthException
2223
import com.firebase.ui.auth.compose.AuthState
@@ -131,6 +132,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
131132
`when`(
132133
mockCredentialManagerProvider.getGoogleCredential(
133134
context = eq(applicationContext),
135+
credentialManager = any<CredentialManager>(),
134136
serverClientId = eq("test-client-id"),
135137
filterByAuthorizedAccounts = eq(true),
136138
autoSelectEnabled = eq(false)
@@ -169,6 +171,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
169171
// Verify credential manager was called
170172
verify(mockCredentialManagerProvider).getGoogleCredential(
171173
context = eq(applicationContext),
174+
credentialManager = any<CredentialManager>(),
172175
serverClientId = eq("test-client-id"),
173176
filterByAuthorizedAccounts = eq(true),
174177
autoSelectEnabled = eq(false)
@@ -198,6 +201,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
198201
`when`(
199202
mockCredentialManagerProvider.getGoogleCredential(
200203
context = eq(applicationContext),
204+
credentialManager = any<CredentialManager>(),
201205
serverClientId = eq("test-client-id"),
202206
filterByAuthorizedAccounts = eq(true),
203207
autoSelectEnabled = eq(false)
@@ -242,6 +246,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
242246
// Verify credential manager was called after authorization
243247
verify(mockCredentialManagerProvider).getGoogleCredential(
244248
context = eq(applicationContext),
249+
credentialManager = any<CredentialManager>(),
245250
serverClientId = eq("test-client-id"),
246251
filterByAuthorizedAccounts = eq(true),
247252
autoSelectEnabled = eq(false)
@@ -270,6 +275,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
270275
`when`(
271276
mockCredentialManagerProvider.getGoogleCredential(
272277
context = eq(applicationContext),
278+
credentialManager = any<CredentialManager>(),
273279
serverClientId = eq("test-client-id"),
274280
filterByAuthorizedAccounts = eq(true),
275281
autoSelectEnabled = eq(false)
@@ -336,6 +342,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
336342
`when`(
337343
mockCredentialManagerProvider.getGoogleCredential(
338344
context = eq(applicationContext),
345+
credentialManager = any<CredentialManager>(),
339346
serverClientId = eq("test-client-id"),
340347
filterByAuthorizedAccounts = eq(true),
341348
autoSelectEnabled = eq(false)
@@ -374,6 +381,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
374381
// Verify sign-in continued despite authorization failure
375382
verify(mockCredentialManagerProvider).getGoogleCredential(
376383
context = eq(applicationContext),
384+
credentialManager = any<CredentialManager>(),
377385
serverClientId = eq("test-client-id"),
378386
filterByAuthorizedAccounts = eq(true),
379387
autoSelectEnabled = eq(false)
@@ -387,6 +395,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
387395
`when`(
388396
mockCredentialManagerProvider.getGoogleCredential(
389397
context = eq(applicationContext),
398+
credentialManager = any<CredentialManager>(),
390399
serverClientId = eq("test-client-id"),
391400
filterByAuthorizedAccounts = eq(true),
392401
autoSelectEnabled = eq(false)
@@ -437,6 +446,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
437446
`when`(
438447
mockCredentialManagerProvider.getGoogleCredential(
439448
context = eq(applicationContext),
449+
credentialManager = any<CredentialManager>(),
440450
serverClientId = eq("test-client-id"),
441451
filterByAuthorizedAccounts = eq(true),
442452
autoSelectEnabled = eq(false)
@@ -486,6 +496,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
486496
`when`(
487497
mockCredentialManagerProvider.getGoogleCredential(
488498
context = eq(applicationContext),
499+
credentialManager = any<CredentialManager>(),
489500
serverClientId = eq("test-client-id"),
490501
filterByAuthorizedAccounts = eq(true),
491502
autoSelectEnabled = eq(false)
@@ -547,6 +558,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
547558
`when`(
548559
mockCredentialManagerProvider.getGoogleCredential(
549560
context = eq(applicationContext),
561+
credentialManager = any<CredentialManager>(),
550562
serverClientId = eq("test-client-id"),
551563
filterByAuthorizedAccounts = eq(true),
552564
autoSelectEnabled = eq(false)
@@ -605,6 +617,7 @@ class GoogleAuthProviderFirebaseAuthUITest {
605617
`when`(
606618
mockCredentialManagerProvider.getGoogleCredential(
607619
context = eq(applicationContext),
620+
credentialManager = any<CredentialManager>(),
608621
serverClientId = eq("test-client-id"),
609622
filterByAuthorizedAccounts = eq(true),
610623
autoSelectEnabled = eq(false)

composeapp/src/main/java/com/firebase/composeapp/MainActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import com.google.firebase.FirebaseApp
3131
*/
3232
class MainActivity : ComponentActivity() {
3333
companion object {
34-
private const val USE_AUTH_EMULATOR = false
34+
private const val USE_AUTH_EMULATOR = true
3535
private const val AUTH_EMULATOR_HOST = "10.0.2.2"
3636
private const val AUTH_EMULATOR_PORT = 9099
3737
}

e2eTest/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies {
5656
testImplementation(Config.Libs.Test.mockitoInline)
5757
testImplementation(Config.Libs.Test.mockitoKotlin)
5858
testImplementation(Config.Libs.Androidx.credentials)
59+
testImplementation(Config.Libs.Misc.googleid)
5960
testImplementation(Config.Libs.Test.composeUiTestJunit4)
6061
}
6162

e2eTest/src/test/java/com/firebase/ui/auth/compose/ui/screens/AnonymousAuthScreenTest.kt

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringPr
3333
import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider
3434
import com.firebase.ui.auth.compose.testutil.AUTH_STATE_WAIT_TIMEOUT_MS
3535
import com.firebase.ui.auth.compose.testutil.EmulatorAuthApi
36+
import com.firebase.ui.auth.compose.testutil.ensureFreshUser
3637
import com.google.common.truth.Truth.assertThat
3738
import com.google.firebase.FirebaseApp
3839
import com.google.firebase.FirebaseOptions
@@ -275,6 +276,133 @@ class AnonymousAuthScreenTest {
275276
.isEqualTo(email)
276277
}
277278

279+
@Test
280+
fun `anonymous upgrade with existing email shows dialog with AccountLinking message`() {
281+
val name = "Existing User"
282+
val email = "[email protected]"
283+
val password = "Password123!"
284+
285+
// Step 1: Create an email/password account first
286+
println("TEST: Creating email/password account...")
287+
ensureFreshUser(authUI, email, password)
288+
println("TEST: Email/password account created")
289+
290+
// Step 2: Sign out
291+
authUI.auth.signOut()
292+
shadowOf(Looper.getMainLooper()).idle()
293+
assertThat(authUI.auth.currentUser).isNull()
294+
println("TEST: Signed out, but email account still exists in emulator")
295+
296+
// Step 3: Sign in anonymously
297+
val configuration = authUIConfiguration {
298+
context = applicationContext
299+
providers {
300+
provider(AuthProvider.Anonymous)
301+
provider(
302+
AuthProvider.Email(
303+
emailLinkActionCodeSettings = null,
304+
passwordValidationRules = emptyList()
305+
)
306+
)
307+
}
308+
isAnonymousUpgradeEnabled = true
309+
}
310+
311+
var currentAuthState: AuthState = AuthState.Idle
312+
313+
composeTestRule.setContent {
314+
TestAuthScreen(configuration = configuration)
315+
val authState by authUI.authStateFlow().collectAsState(AuthState.Idle)
316+
currentAuthState = authState
317+
}
318+
319+
composeTestRule.waitForIdle()
320+
shadowOf(Looper.getMainLooper()).idle()
321+
322+
println("TEST: Clicking anonymous sign-in button...")
323+
composeTestRule.onNodeWithText(stringProvider.signInAnonymously)
324+
.assertIsDisplayed()
325+
.performClick()
326+
327+
composeTestRule.waitForIdle()
328+
shadowOf(Looper.getMainLooper()).idle()
329+
330+
// Wait for anonymous auth to complete
331+
println("TEST: Waiting for anonymous auth...")
332+
composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) {
333+
shadowOf(Looper.getMainLooper()).idle()
334+
println("TEST: Auth state: $currentAuthState")
335+
currentAuthState is AuthState.Success
336+
}
337+
338+
assertThat(authUI.auth.currentUser!!.isAnonymous).isTrue()
339+
val anonymousUserUID = authUI.auth.currentUser!!.uid
340+
println("TEST: Anonymous user UID: $anonymousUserUID")
341+
342+
// Step 4: Try to upgrade with existing email
343+
println("TEST: Clicking 'Upgrade with Email' button...")
344+
composeTestRule.onNodeWithText("Upgrade with Email")
345+
.assertIsDisplayed()
346+
.performClick()
347+
348+
composeTestRule.waitForIdle()
349+
shadowOf(Looper.getMainLooper()).idle()
350+
351+
// Navigate to sign-up
352+
println("TEST: Navigating to sign-up...")
353+
composeTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase())
354+
.assertIsDisplayed()
355+
.performClick()
356+
357+
// Enter the existing email
358+
println("TEST: Entering existing email...")
359+
composeTestRule.onNodeWithText(stringProvider.emailHint)
360+
.assertIsDisplayed()
361+
.performTextInput(email)
362+
composeTestRule.onNodeWithText(stringProvider.nameHint)
363+
.assertIsDisplayed()
364+
.performTextInput(name)
365+
composeTestRule.onNodeWithText(stringProvider.passwordHint)
366+
.performScrollTo()
367+
.assertIsDisplayed()
368+
.performTextInput(password)
369+
composeTestRule.onNodeWithText(stringProvider.confirmPasswordHint)
370+
.performScrollTo()
371+
.assertIsDisplayed()
372+
.performTextInput(password)
373+
374+
// Click sign-up button
375+
println("TEST: Clicking sign-up button...")
376+
composeTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase())
377+
.performScrollTo()
378+
.assertIsDisplayed()
379+
.performClick()
380+
381+
composeTestRule.waitForIdle()
382+
shadowOf(Looper.getMainLooper()).idle()
383+
384+
// Step 5: Wait for error state (AccountLinkingRequiredException)
385+
println("TEST: Waiting for AccountLinkingRequiredException...")
386+
composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) {
387+
shadowOf(Looper.getMainLooper()).idle()
388+
println("TEST: Auth state: $currentAuthState")
389+
currentAuthState is AuthState.Error
390+
}
391+
392+
// Step 6: Verify ErrorRecoveryDialog is displayed
393+
println("TEST: Verifying ErrorRecoveryDialog is displayed...")
394+
composeTestRule.onNodeWithText(stringProvider.errorDialogTitle)
395+
.assertIsDisplayed()
396+
397+
// Verify exception
398+
assertThat(currentAuthState).isInstanceOf(AuthState.Error::class.java)
399+
val errorState = currentAuthState as AuthState.Error
400+
assertThat(errorState.exception).isInstanceOf(AuthException.AccountLinkingRequiredException::class.java)
401+
402+
val linkingException = errorState.exception as AuthException.AccountLinkingRequiredException
403+
assertThat(linkingException.email).isEqualTo(email)
404+
}
405+
278406
@Composable
279407
private fun TestAuthScreen(configuration: AuthUIConfiguration) {
280408
composeTestRule.waitForIdle()

0 commit comments

Comments
 (0)