Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import com.google.firebase.app
import com.google.firebase.initialize
import com.google.firebase.util.nextAlphanumericString
import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
* A JUnit test rule that creates instances of [FirebaseApp] for use during testing, and closes them
Expand All @@ -37,6 +43,12 @@ class TestFirebaseAppFactory : FactoryTestRule<FirebaseApp, Nothing>() {
)

override fun destroyInstance(instance: FirebaseApp) {
instance.delete()
// Work around app crash due to IllegalStateException from FirebaseAuth if `delete()` is called
// very quickly after `FirebaseApp.getInstance()`. See b/378116261 for details.
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
delay(1.seconds)
instance.delete()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, L : Any>(

if (state.compareAndSet(oldState, State.Closed)) {
providerListenerPair?.run {
provider?.let { provider ->
runIgnoringFirebaseAppDeleted { removeTokenListener(provider, tokenListener) }
}
provider?.let { provider -> removeTokenListener(provider, tokenListener) }
}
return
}
Expand Down Expand Up @@ -422,7 +420,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, L : Any>(
@DeferredApi
private fun onProviderAvailable(newProvider: T, tokenListener: L) {
logger.debug { "onProviderAvailable(newProvider=$newProvider)" }
runIgnoringFirebaseAppDeleted { addTokenListener(newProvider, tokenListener) }
addTokenListener(newProvider, tokenListener)

while (true) {
val oldState = state.get()
Expand All @@ -437,7 +435,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, L : Any>(
"onProviderAvailable(newProvider=$newProvider)" +
" unregistering token listener that was just added"
}
runIgnoringFirebaseAppDeleted { removeTokenListener(newProvider, tokenListener) }
removeTokenListener(newProvider, tokenListener)
break
}
is State.Ready ->
Expand Down Expand Up @@ -486,20 +484,6 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, L : Any>(
private class GetTokenCancelledException(cause: Throwable) :
DataConnectException("getToken() was cancelled, likely by close()", cause)

// Work around a race condition where addIdTokenListener() and removeIdTokenListener() throw if
// the FirebaseApp is deleted during or before its invocation.
private fun runIgnoringFirebaseAppDeleted(block: () -> Unit) {
try {
block()
} catch (e: IllegalStateException) {
if (e.message == "FirebaseApp was deleted") {
logger.warn(e) { "ignoring exception: $e" }
} else {
throw e
}
}
}

protected data class GetTokenResult(val token: String?)

private companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,43 +552,6 @@ class DataConnectAuthUnitTest {
)
}

@Test
fun `addIdTokenListener() throwing IllegalStateException due to FirebaseApp deleted should be ignored`() =
runTest {
every { mockInternalAuthProvider.addIdTokenListener(any()) } throws
firebaseAppDeletedException
coEvery { mockInternalAuthProvider.getAccessToken(any()) } returns taskForToken(accessToken)
val dataConnectAuth = newDataConnectAuth()
dataConnectAuth.initialize()
advanceUntilIdle()

eventually(`check every 100 milliseconds for 2 seconds`) {
mockLogger.shouldHaveLoggedExactlyOneMessageContaining(
"ignoring exception: $firebaseAppDeletedException"
)
}
val result = dataConnectAuth.getToken(requestId)
withClue("result=$result") { result shouldBe accessToken }
}

@Test
fun `removeIdTokenListener() throwing IllegalStateException due to FirebaseApp deleted should be ignored`() =
runTest {
every { mockInternalAuthProvider.removeIdTokenListener(any()) } throws
firebaseAppDeletedException
val dataConnectAuth = newDataConnectAuth()
dataConnectAuth.initialize()
advanceUntilIdle()

dataConnectAuth.close()

eventually(`check every 100 milliseconds for 2 seconds`) {
mockLogger.shouldHaveLoggedExactlyOneMessageContaining(
"ignoring exception: $firebaseAppDeletedException"
)
}
}

private fun TestScope.newDataConnectAuth(
deferredInternalAuthProvider: DeferredInternalAuthProvider =
ImmediateDeferred(mockInternalAuthProvider),
Expand Down
Loading