Skip to content

Commit d2becd6

Browse files
committed
chore: improve test utils
1 parent 3d63799 commit d2becd6

File tree

1 file changed

+111
-77
lines changed

1 file changed

+111
-77
lines changed

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

Lines changed: 111 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import com.google.firebase.FirebaseOptions
2929
import com.google.firebase.auth.AuthResult
3030
import com.google.firebase.auth.FirebaseUser
3131
import com.google.firebase.auth.actionCodeSettings
32+
import org.json.JSONArray
33+
import org.json.JSONObject
3234
import org.junit.After
3335
import org.junit.Before
3436
import org.junit.Rule
@@ -41,6 +43,8 @@ import org.robolectric.annotation.Config
4143
import java.net.HttpURLConnection
4244
import java.net.URL
4345

46+
private const val AUTH_STATE_WAIT_TIMEOUT_MS = 5_000L
47+
4448
@Config(sdk = [34])
4549
@RunWith(RobolectricTestRunner::class)
4650
class EmailAuthScreenTest {
@@ -52,6 +56,7 @@ class EmailAuthScreenTest {
5256
private lateinit var stringProvider: AuthUIStringProvider
5357

5458
lateinit var authUI: FirebaseAuthUI
59+
private lateinit var emulatorApi: EmulatorAuthApi
5560

5661
@Before
5762
fun setUp() {
@@ -67,7 +72,7 @@ class EmailAuthScreenTest {
6772
}
6873

6974
// Initialize default FirebaseApp
70-
FirebaseApp.initializeApp(
75+
val firebaseApp = FirebaseApp.initializeApp(
7176
applicationContext,
7277
FirebaseOptions.Builder()
7378
.setApiKey("fake-api-key")
@@ -79,6 +84,13 @@ class EmailAuthScreenTest {
7984
authUI = FirebaseAuthUI.getInstance()
8085
authUI.auth.useEmulator("127.0.0.1", 9099)
8186

87+
emulatorApi = EmulatorAuthApi(
88+
projectId = firebaseApp.options.projectId
89+
?: throw IllegalStateException("Project ID is required for emulator interactions"),
90+
emulatorHost = "127.0.0.1",
91+
emulatorPort = 9099
92+
)
93+
8294
// Clear emulator data
8395
clearEmulatorData()
8496
}
@@ -165,7 +177,7 @@ class EmailAuthScreenTest {
165177

166178
// Wait for auth state to transition to RequiresEmailVerification
167179
println("TEST: Waiting for auth state change... Current state: $currentAuthState")
168-
composeTestRule.waitUntil {
180+
composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) {
169181
shadowOf(Looper.getMainLooper()).idle()
170182
println("TEST: Auth state during wait: $currentAuthState")
171183
currentAuthState is AuthState.RequiresEmailVerification
@@ -239,7 +251,7 @@ class EmailAuthScreenTest {
239251

240252
// Wait for auth state to transition to Success (since email is verified)
241253
println("TEST: Waiting for auth state change... Current state: $currentAuthState")
242-
composeTestRule.waitUntil(timeoutMillis = 5000) {
254+
composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) {
243255
shadowOf(Looper.getMainLooper()).idle()
244256
println("TEST: Auth state during wait: $currentAuthState")
245257
currentAuthState is AuthState.Success
@@ -319,7 +331,7 @@ class EmailAuthScreenTest {
319331

320332
// Wait for auth state to transition to RequiresEmailVerification
321333
println("TEST: Waiting for auth state change... Current state: $currentAuthState")
322-
composeTestRule.waitUntil {
334+
composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) {
323335
shadowOf(Looper.getMainLooper()).idle()
324336
println("TEST: Auth state during wait: $currentAuthState")
325337
currentAuthState is AuthState.RequiresEmailVerification
@@ -391,7 +403,7 @@ class EmailAuthScreenTest {
391403

392404
// Wait for auth state to transition to PasswordResetLinkSent
393405
println("TEST: Waiting for auth state change... Current state: $currentAuthState")
394-
composeTestRule.waitUntil {
406+
composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) {
395407
shadowOf(Looper.getMainLooper()).idle()
396408
println("TEST: Auth state during wait: $currentAuthState")
397409
currentAuthState is AuthState.PasswordResetLinkSent
@@ -479,7 +491,7 @@ class EmailAuthScreenTest {
479491

480492
// Wait for auth state to transition to EmailSignInLinkSent
481493
println("TEST: Waiting for auth state change... Current state: $currentAuthState")
482-
composeTestRule.waitUntil {
494+
composeTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) {
483495
shadowOf(Looper.getMainLooper()).idle()
484496
println("TEST: Auth state during wait: $currentAuthState")
485497
currentAuthState is AuthState.EmailSignInLinkSent
@@ -576,36 +588,14 @@ class EmailAuthScreenTest {
576588
* This function calls the emulator's clear data endpoint to remove all accounts,
577589
* OOB codes, and other authentication data. This ensures test isolation by providing
578590
* a clean slate for each test.
579-
*
580-
* @param emulatorHost The emulator host (default: "127.0.0.1")
581-
* @param emulatorPort The emulator port (default: 9099)
582-
*
583-
* @throws Exception if the clear operation fails
584591
*/
585-
private fun clearEmulatorData(
586-
projectId: String = "fake-project-id",
587-
emulatorHost: String = "127.0.0.1",
588-
emulatorPort: Int = 9099
589-
) {
590-
val clearUrl =
591-
URL("http://$emulatorHost:$emulatorPort/emulator/v1/projects/$projectId/accounts")
592-
val clearConnection = clearUrl.openConnection() as HttpURLConnection
593-
594-
try {
595-
clearConnection.requestMethod = "DELETE"
596-
clearConnection.connectTimeout = 5000
597-
clearConnection.readTimeout = 5000
598-
599-
val responseCode = clearConnection.responseCode
600-
if (responseCode !in 200..299) {
601-
println("WARNING: Failed to clear emulator data: HTTP $responseCode")
602-
} else {
603-
println("TEST: Cleared emulator data")
592+
private fun clearEmulatorData() {
593+
if (::emulatorApi.isInitialized) {
594+
try {
595+
emulatorApi.clearAccounts()
596+
} catch (e: Exception) {
597+
println("WARNING: Exception while clearing emulator data: ${e.message}")
604598
}
605-
} catch (e: Exception) {
606-
println("WARNING: Exception while clearing emulator data: ${e.message}")
607-
} finally {
608-
clearConnection.disconnect()
609599
}
610600
}
611601

@@ -649,16 +639,9 @@ class EmailAuthScreenTest {
649639
* the real email verification flow that would occur in production.
650640
*
651641
* @param user The FirebaseUser whose email should be verified
652-
* @param emulatorHost The emulator host (default: "127.0.0.1")
653-
* @param emulatorPort The emulator port (default: 9099)
654-
*
655642
* @throws Exception if the verification flow fails
656643
*/
657-
private fun verifyEmailInEmulator(
658-
user: FirebaseUser,
659-
emulatorHost: String = "127.0.0.1",
660-
emulatorPort: Int = 9099
661-
) {
644+
private fun verifyEmailInEmulator(user: FirebaseUser) {
662645
println("TEST: Starting email verification for user ${user.uid}")
663646

664647
// Step 1: Send verification email to generate an OOB code
@@ -669,54 +652,105 @@ class EmailAuthScreenTest {
669652
shadowOf(Looper.getMainLooper()).idle()
670653
Thread.sleep(100)
671654

672-
// Step 2: Retrieve OOB codes from the emulator
673-
val projectId = authUI.app.options.projectId
674-
?: throw IllegalStateException("Project ID is required")
675-
println("TEST: Using project ID: $projectId")
655+
// Step 2: Retrieve the VERIFY_EMAIL OOB code for this user from the emulator
656+
val email = requireNotNull(user.email) { "User email is required for OOB code lookup" }
657+
val oobCode = emulatorApi.fetchVerifyEmailCode(email)
658+
659+
println("TEST: Found OOB code: $oobCode")
660+
661+
// Step 3: Apply the action code to verify the email
662+
authUI.auth.applyActionCode(oobCode).awaitWithLooper()
663+
println("TEST: Applied action code")
664+
665+
// Step 4: Reload the user to refresh their email verification status
666+
authUI.auth.currentUser?.reload()?.awaitWithLooper()
667+
shadowOf(Looper.getMainLooper()).idle()
668+
669+
println("TEST: Email verified successfully for user ${user.uid}")
670+
println("TEST: User isEmailVerified: ${authUI.auth.currentUser?.isEmailVerified}")
671+
}
676672

677-
val oobUrl =
678-
URL("http://$emulatorHost:$emulatorPort/emulator/v1/projects/$projectId/oobCodes")
679-
val oobConnection = oobUrl.openConnection() as HttpURLConnection
673+
}
680674

681-
val oobCodesJson = try {
682-
oobConnection.requestMethod = "GET"
683-
oobConnection.connectTimeout = 5000
684-
oobConnection.readTimeout = 5000
675+
private class EmulatorAuthApi(
676+
private val projectId: String,
677+
emulatorHost: String,
678+
emulatorPort: Int
679+
) {
685680

686-
val responseCode = oobConnection.responseCode
687-
if (responseCode != 200) {
681+
private val httpClient = HttpClient(host = emulatorHost, port = emulatorPort)
682+
683+
fun clearAccounts() {
684+
httpClient.delete("/emulator/v1/projects/$projectId/accounts") { connection ->
685+
val responseCode = connection.responseCode
686+
if (responseCode !in 200..299) {
687+
println("WARNING: Failed to clear emulator data: HTTP $responseCode")
688+
} else {
689+
println("TEST: Cleared emulator data")
690+
}
691+
}
692+
}
693+
694+
fun fetchVerifyEmailCode(email: String): String {
695+
val oobCodes = fetchOobCodes()
696+
return (0 until oobCodes.length())
697+
.asSequence()
698+
.mapNotNull { index -> oobCodes.optJSONObject(index) }
699+
.firstOrNull { json ->
700+
json.optString("email") == email &&
701+
json.optString("requestType") == "VERIFY_EMAIL"
702+
}
703+
?.optString("oobCode")
704+
?.takeIf { it.isNotBlank() }
705+
?: throw Exception("No VERIFY_EMAIL OOB code found for user email: $email")
706+
}
707+
708+
private fun fetchOobCodes(): JSONArray {
709+
val payload = httpClient.get("/emulator/v1/projects/$projectId/oobCodes") { connection ->
710+
val responseCode = connection.responseCode
711+
if (responseCode != HttpURLConnection.HTTP_OK) {
688712
throw Exception("Failed to get OOB codes: HTTP $responseCode")
689713
}
690714

691-
oobConnection.inputStream.bufferedReader().readText()
692-
} finally {
693-
oobConnection.disconnect()
715+
connection.inputStream.bufferedReader().use { it.readText() }
694716
}
695717

696-
println("TEST: OOB codes response: $oobCodesJson")
718+
return JSONObject(payload).optJSONArray("oobCodes") ?: JSONArray()
719+
}
720+
}
697721

698-
// Step 3: Parse the response to find the VERIFY_EMAIL code for this user's email
699-
// Response format: {"oobCodes": [{"email": "...", "oobCode": "...", "oobLink": "...", "requestType": "..."}]}
700-
// We need to find an entry with both matching email AND requestType: "VERIFY_EMAIL"
701-
val verifyEmailPattern =
702-
""""email":"${user.email}","requestType":"VERIFY_EMAIL","oobCode":"([^"]+)"""".toRegex()
722+
private class HttpClient(
723+
private val host: String,
724+
private val port: Int,
725+
private val timeoutMs: Int = DEFAULT_TIMEOUT_MS
726+
) {
703727

704-
val oobCodeMatch = verifyEmailPattern.find(oobCodesJson)
705-
val oobCode = oobCodeMatch?.groupValues?.get(1)
706-
?: throw Exception("No VERIFY_EMAIL OOB code found for user email: ${user.email}")
728+
companion object {
729+
const val DEFAULT_TIMEOUT_MS = 5_000
730+
}
707731

708-
println("TEST: Found OOB code: $oobCode")
732+
fun delete(path: String, block: (HttpURLConnection) -> Unit) {
733+
execute(path = path, method = "DELETE", block = block)
734+
}
709735

710-
// Step 4: Apply the action code to verify the email
711-
authUI.auth.applyActionCode(oobCode).awaitWithLooper()
712-
println("TEST: Applied action code")
736+
fun <T> get(path: String, block: (HttpURLConnection) -> T): T {
737+
return execute(path = path, method = "GET", block = block)
738+
}
713739

714-
// Step 5: Reload the user to refresh their email verification status
715-
authUI.auth.currentUser?.reload()?.awaitWithLooper()
716-
shadowOf(Looper.getMainLooper()).idle()
740+
private fun <T> execute(path: String, method: String, block: (HttpURLConnection) -> T): T {
741+
val connection = buildUrl(path).openConnection() as HttpURLConnection
742+
connection.requestMethod = method
743+
connection.connectTimeout = timeoutMs
744+
connection.readTimeout = timeoutMs
717745

718-
println("TEST: Email verified successfully for user ${user.uid}")
719-
println("TEST: User isEmailVerified: ${authUI.auth.currentUser?.isEmailVerified}")
746+
return try {
747+
block(connection)
748+
} finally {
749+
connection.disconnect()
750+
}
720751
}
721752

722-
}
753+
private fun buildUrl(path: String): URL {
754+
return URL("http://$host:$port$path")
755+
}
756+
}

0 commit comments

Comments
 (0)