From e19276822f2158fe3f6e24fa54a25a30dc9ae9e2 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Apr 2026 12:20:45 -0400 Subject: [PATCH 1/2] dataconnect(test): fix test flakiness in AuthIntegrationTest.kt --- .../dataconnect/AuthIntegrationTest.kt | 29 ++++++++------- .../InProcessDataConnectGrpcServer.kt | 37 +++++++++++++------ 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt index 280d5209065..a973e008790 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt @@ -17,7 +17,6 @@ package com.google.firebase.dataconnect import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.dataconnect.core.FirebaseDataConnectInternal import com.google.firebase.dataconnect.testutil.DataConnectBackend import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase import com.google.firebase.dataconnect.testutil.InProcessDataConnectGrpcServer @@ -44,6 +43,7 @@ import io.kotest.property.Arb import io.kotest.property.arbitrary.next import java.util.concurrent.CopyOnWriteArrayList import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toCollection import kotlinx.coroutines.launch @@ -70,10 +70,10 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { @Test fun authenticatedRequestsAreSuccessful() = runTest { - signIn() val person1Id = Arb.alphanumericString(prefix = "person1Id").next() val person2Id = Arb.alphanumericString(prefix = "person2Id").next() val person3Id = Arb.alphanumericString(prefix = "person3Id").next() + signIn(personSchema.dataConnect) personSchema.createPersonAuth(id = person1Id, name = "TestName1", age = 42).execute() personSchema.createPersonAuth(id = person2Id, name = "TestName2", age = 43).execute() @@ -85,7 +85,7 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { @Test fun queryFailsAfterUserSignsOut() = runTest { - signIn() + signIn(personSchema.dataConnect) // Verify that we are signed in by executing a query, which should succeed. personSchema.getPersonAuth(id = "foo").execute() signOut() @@ -98,7 +98,7 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { @Test fun mutationFailsAfterUserSignsOut() = runTest { - signIn() + signIn(personSchema.dataConnect) // Verify that we are signed in by executing a mutation, which should succeed. personSchema.createPersonAuth(id = Random.nextAlphanumericString(20), name = "foo").execute() signOut() @@ -115,20 +115,20 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { @Test fun queryShouldRetryOnUnauthenticated() = runTest { - signIn() val responseData = buildStructProto { put("foo", key) } val executeQueryResponse = executeQueryResponse { data = responseData } val grpcServer = inProcessDataConnectGrpcServer.newInstance( errors = listOf(Status.UNAUTHENTICATED), - executeQueryResponse = executeQueryResponse + executeQueryResponse = executeQueryResponse, + responseDelay = 1.seconds, // avoid getting the same access token from auth emulator ) val authTokens = CopyOnWriteArrayList() backgroundScope.launch { grpcServer.metadatas.map { it.get(firebaseAuthTokenHeader) }.toCollection(authTokens) } val dataConnect = dataConnectFactory.newInstance(auth.app, grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAuthReady() + signIn(dataConnect) val operationName = Arb.dataConnect.operationName().next(rs) val queryRef = dataConnect.query(operationName, Unit, serializer(), serializer()) @@ -144,20 +144,20 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { @Test fun mutationShouldRetryOnUnauthenticated() = runTest { - signIn() val responseData = buildStructProto { put("foo", key) } val executeMutationResponse = executeMutationResponse { data = responseData } val grpcServer = inProcessDataConnectGrpcServer.newInstance( errors = listOf(Status.UNAUTHENTICATED), - executeMutationResponse = executeMutationResponse + executeMutationResponse = executeMutationResponse, + responseDelay = 1.seconds, // avoid getting the same access token from auth emulator ) val authTokens = CopyOnWriteArrayList() backgroundScope.launch { grpcServer.metadatas.map { it.get(firebaseAuthTokenHeader) }.toCollection(authTokens) } val dataConnect = dataConnectFactory.newInstance(auth.app, grpcServer) - (dataConnect as FirebaseDataConnectInternal).awaitAuthReady() + signIn(dataConnect) val operationName = Arb.dataConnect.operationName().next(rs) val mutationRef = dataConnect.mutation(operationName, Unit, serializer(), serializer()) @@ -173,12 +173,12 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { @Test fun queryShouldOnlyRetryOnUnauthenticatedOnce() = runTest { - signIn() val grpcServer = inProcessDataConnectGrpcServer.newInstance( errors = listOf(Status.UNAUTHENTICATED, Status.UNAUTHENTICATED), ) val dataConnect = dataConnectFactory.newInstance(auth.app, grpcServer) + signIn(dataConnect) val operationName = Arb.dataConnect.operationName().next(rs) val queryRef = dataConnect.query(operationName, Unit, serializer(), serializer()) @@ -189,12 +189,13 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { @Test fun mutationShouldOnlyRetryOnUnauthenticatedOnce() = runTest { - signIn() val grpcServer = inProcessDataConnectGrpcServer.newInstance( errors = listOf(Status.UNAUTHENTICATED, Status.UNAUTHENTICATED), + responseDelay = 1.seconds, // avoid getting the same access token from auth emulator ) val dataConnect = dataConnectFactory.newInstance(auth.app, grpcServer) + signIn(dataConnect) val operationName = Arb.dataConnect.operationName().next(rs) val mutationRef = dataConnect.mutation(operationName, Unit, serializer(), serializer()) @@ -204,8 +205,8 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { thrownException.asClue { it.status shouldBe Status.UNAUTHENTICATED } } - private suspend fun signIn() { - personSchema.dataConnect.awaitAuthReady() + private suspend fun signIn(dataConnect: FirebaseDataConnect) { + dataConnect.awaitAuthReady() val authResult = auth.run { signInAnonymously().await() } withClue("authResult.user returned from signInAnonymously()") { authResult.user.shouldNotBeNull() diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/InProcessDataConnectGrpcServer.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/InProcessDataConnectGrpcServer.kt index 9598c09fd6d..9b73cb2d88b 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/InProcessDataConnectGrpcServer.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/InProcessDataConnectGrpcServer.kt @@ -36,6 +36,7 @@ import io.grpc.Status import io.grpc.StatusException import io.grpc.okhttp.OkHttpServerBuilder import io.grpc.stub.StreamObserver +import kotlin.time.Duration import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -53,32 +54,40 @@ class InProcessDataConnectGrpcServer : fun newInstance( errors: List? = null, executeQueryResponse: ExecuteQueryResponse? = null, - executeMutationResponse: ExecuteMutationResponse? = null + executeMutationResponse: ExecuteMutationResponse? = null, + responseDelay: Duration? = null, ): ServerInfo = createInstance( errors = errors, executeQueryResponse = executeQueryResponse, - executeMutationResponse = executeMutationResponse + executeMutationResponse = executeMutationResponse, + responseDelay = responseDelay, ) override fun createInstance(params: Params?): ServerInfo { return createInstance( params?.errors, params?.executeQueryResponse, - params?.executeMutationResponse + params?.executeMutationResponse, + params?.responseDelay, ) } private fun createInstance( - errors: List? = null, - executeQueryResponse: ExecuteQueryResponse? = null, - executeMutationResponse: ExecuteMutationResponse? = null + errors: List?, + executeQueryResponse: ExecuteQueryResponse?, + executeMutationResponse: ExecuteMutationResponse?, + responseDelay: Duration?, ): ServerInfo { - val serverInterceptor = ServerInterceptorImpl(errors ?: Params.defaults.errors) + val serverInterceptor = + ServerInterceptorImpl( + errors ?: Params.defaults.errors, + responseDelay ?: Params.defaults.responseDelay, + ) val connectorService = ConnectorServiceImpl( executeQueryResponse ?: Params.defaults.executeQueryResponse, - executeMutationResponse ?: Params.defaults.executeMutationResponse + executeMutationResponse ?: Params.defaults.executeMutationResponse, ) val grpcServer = OkHttpServerBuilder.forPort(0, InsecureServerCredentials.create()) @@ -92,7 +101,8 @@ class InProcessDataConnectGrpcServer : data class Params( val errors: List = emptyList(), val executeQueryResponse: ExecuteQueryResponse? = null, - val executeMutationResponse: ExecuteMutationResponse? = null + val executeMutationResponse: ExecuteMutationResponse? = null, + val responseDelay: Duration? = null, ) { companion object { val defaults = Params() @@ -105,7 +115,8 @@ class InProcessDataConnectGrpcServer : data class ServerInfo(val server: Server, val metadatas: Flow) - private class ServerInterceptorImpl(errors: List = emptyList()) : ServerInterceptor { + private class ServerInterceptorImpl(errors: List, private val responseDelay: Duration?) : + ServerInterceptor { private val errors = errors.toList().iterator() @@ -121,6 +132,8 @@ class InProcessDataConnectGrpcServer : ): ServerCall.Listener { check(_metadatas.tryEmit(headers)) { "_metadatas.tryEmit(headers) failed" } + responseDelay?.let { Thread.sleep(it.inWholeMilliseconds) } + synchronized(errors) { if (errors.hasNext()) { throw StatusException(errors.next()) @@ -132,8 +145,8 @@ class InProcessDataConnectGrpcServer : } private class ConnectorServiceImpl( - val executeQueryResponse: ExecuteQueryResponse? = null, - val executeMutationResponse: ExecuteMutationResponse? = null + val executeQueryResponse: ExecuteQueryResponse?, + val executeMutationResponse: ExecuteMutationResponse?, ) : ConnectorServiceGrpc.ConnectorServiceImplBase() { override fun executeQuery( request: ExecuteQueryRequest, From 0fefcf1bb3876f8925fa1f17efc8b82e289614cf Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Apr 2026 13:12:37 -0400 Subject: [PATCH 2/2] AuthIntegrationTest.kt: add `responseDelay = 1.seconds` to `queryShouldOnlyRetryOnUnauthenticatedOnce`, which was overlooked --- .../com/google/firebase/dataconnect/AuthIntegrationTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt index a973e008790..3c29454478e 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/AuthIntegrationTest.kt @@ -176,6 +176,7 @@ class AuthIntegrationTest : DataConnectIntegrationTestBase() { val grpcServer = inProcessDataConnectGrpcServer.newInstance( errors = listOf(Status.UNAUTHENTICATED, Status.UNAUTHENTICATED), + responseDelay = 1.seconds, // avoid getting the same access token from auth emulator ) val dataConnect = dataConnectFactory.newInstance(auth.app, grpcServer) signIn(dataConnect)