From cac4680754451d8d92cc4a347d283f06df8e6fd9 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Apr 2026 16:47:17 -0400 Subject: [PATCH 1/4] dataconnect(chore): Pass around GetAuthTokenResult and GetAppCheckTokenResult objects instead of raw strings for better type safety [skip actions] --- firebase-dataconnect/CHANGELOG.md | 2 + .../dataconnect/core/DataConnectGrpcClient.kt | 25 +- .../core/DataConnectGrpcMetadata.kt | 73 ++-- .../dataconnect/core/DataConnectGrpcRPCs.kt | 22 +- .../core/FirebaseDataConnectImpl.kt | 2 - .../core/DataConnectAuthUnitTest.kt | 2 +- .../core/DataConnectGrpcClientUnitTest.kt | 352 ++++++++------- .../core/DataConnectGrpcMetadataUnitTest.kt | 404 +++++++++++------- .../core/DataConnectGrpcRPCsUnitTest.kt | 62 ++- .../testutil/property/arbitrary/arbs.kt | 17 +- .../testutil/property/arbitrary/arbs.kt | 10 +- .../testutil/property/arbitrary/tuples.kt | 28 +- 12 files changed, 602 insertions(+), 397 deletions(-) diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index b6eb119fb90..6639a3433e4 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -9,6 +9,8 @@ ([#7957](https://github.com/firebase/firebase-android-sdk/pull/7957)) - [changed] Internal refactor for calculating debug logging strings. ([#8024](https://github.com/firebase/firebase-android-sdk/pull/8024)) +- [changed] Internal refactor to use token objects instead of strings. + ([#NNNN](https://github.com/firebase/firebase-android-sdk/pull/NNNN)) # 17.2.0 diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt index ecb783e163f..447051c0ff0 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClient.kt @@ -20,6 +20,8 @@ import com.google.firebase.dataconnect.* import com.google.firebase.dataconnect.DataConnectPathSegment import com.google.firebase.dataconnect.DataSource import com.google.firebase.dataconnect.QueryRef.FetchPolicy +import com.google.firebase.dataconnect.core.DataConnectAppCheck.GetAppCheckTokenResult +import com.google.firebase.dataconnect.core.DataConnectAuth.GetAuthTokenResult import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.toErrorInfoImpl import com.google.firebase.dataconnect.core.LoggerGlobals.warn import com.google.firebase.dataconnect.util.ProtoUtil.decodeFromStruct @@ -73,8 +75,9 @@ internal class DataConnectGrpcClient( } val executeQueryResult = - grpcRPCs.retryOnGrpcUnauthenticatedError(requestId, "executeQuery") { - executeQuery(requestId, request, callerSdkType, fetchPolicy) + grpcRPCs.retryOnGrpcUnauthenticatedError(requestId, "executeQuery") { authToken, appCheckToken + -> + executeQuery(requestId, request, callerSdkType, fetchPolicy, authToken, appCheckToken) } return executeQueryResult.toOperationResult() @@ -94,7 +97,9 @@ internal class DataConnectGrpcClient( val response = grpcRPCs.retryOnGrpcUnauthenticatedError(requestId, "executeMutation") { - executeMutation(requestId, request, callerSdkType) + authToken, + appCheckToken -> + executeMutation(requestId, request, callerSdkType, authToken, appCheckToken) } return OperationResult( @@ -104,13 +109,16 @@ internal class DataConnectGrpcClient( ) } - private inline fun T.retryOnGrpcUnauthenticatedError( + private suspend inline fun T.retryOnGrpcUnauthenticatedError( requestId: String, kotlinMethodName: String, - block: T.() -> R + block: T.(GetAuthTokenResult?, GetAppCheckTokenResult?) -> R, ): R { + val authToken1 = dataConnectAuth.getToken(requestId) + val appCheckToken1 = dataConnectAppCheck.getToken(requestId) + return try { - block() + block(authToken1, appCheckToken1) } catch (e: StatusException) { if (e.status.code != Status.UNAUTHENTICATED.code) { throw e @@ -125,7 +133,10 @@ internal class DataConnectGrpcClient( dataConnectAuth.forceRefresh() dataConnectAppCheck.forceRefresh() - block() + val authToken2 = dataConnectAuth.getToken(requestId) + val appCheckToken2 = dataConnectAppCheck.getToken(requestId) + + block(authToken2, appCheckToken2) } } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt index d63566253f1..a0569260028 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt @@ -24,12 +24,11 @@ import com.google.firebase.dataconnect.core.Globals.toScrubbedAccessToken import com.google.firebase.dataconnect.core.LoggerGlobals.Logger import com.google.firebase.dataconnect.core.LoggerGlobals.debug import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto +import com.google.firebase.dataconnect.util.StructProtoBuilder import com.google.protobuf.Struct import io.grpc.Metadata internal class DataConnectGrpcMetadata( - val dataConnectAuth: DataConnectAuth, - val dataConnectAppCheck: DataConnectAppCheck, val connectorLocation: String, val kotlinVersion: String, val androidVersion: Int, @@ -42,7 +41,6 @@ internal class DataConnectGrpcMetadata( Logger("DataConnectGrpcMetadata").apply { debug { "created by ${parentLogger.nameWithId} with" + - " dataConnectAuth=${dataConnectAuth.instanceId}" + " connectorLocation=$connectorLocation" + " kotlinVersion=$kotlinVersion" + " androidVersion=$androidVersion" + @@ -54,7 +52,6 @@ internal class DataConnectGrpcMetadata( val instanceId: String get() = logger.nameWithId - @Suppress("SpellCheckingInspection") private val googRequestParamsHeaderValue = "location=${connectorLocation}&frontend=data" private fun googApiClientHeaderValue(callerSdkType: FirebaseDataConnect.CallerSdkType): String { @@ -76,18 +73,11 @@ internal class DataConnectGrpcMetadata( return components.joinToString(" ") } - data class GetGrpcMetadataResult( - val metadata: Metadata, - val authToken: DataConnectAuth.GetAuthTokenResult?, - ) - - suspend fun get( - requestId: String, + fun get( + authToken: DataConnectAuth.GetAuthTokenResult?, + appCheckToken: DataConnectAppCheck.GetAppCheckTokenResult?, callerSdkType: FirebaseDataConnect.CallerSdkType - ): GetGrpcMetadataResult { - val authToken = dataConnectAuth.getToken(requestId) - val appCheckToken = dataConnectAppCheck.getToken(requestId) - + ): Metadata { val metadata = Metadata().also { it.put(googRequestParamsHeader, googRequestParamsHeaderValue) @@ -95,15 +85,22 @@ internal class DataConnectGrpcMetadata( if (appId.isNotBlank()) { it.put(gmpAppIdHeader, appId) } - authToken?.token?.let { token -> it.put(firebaseAuthTokenHeader, token) } - appCheckToken?.token?.let { token -> it.put(firebaseAppCheckTokenHeader, token) } + + authToken?.token?.let { authTokenString -> + it.put(firebaseAuthTokenHeader, authTokenString) + } + appCheckToken?.token?.let { appCheckTokenString -> + it.put(firebaseAppCheckTokenHeader, appCheckTokenString) + } } - return GetGrpcMetadataResult(metadata, authToken) + return metadata } companion object { - fun Metadata.toStructProto(): Struct = buildStructProto { + // TODO: Move this to ProtoUtil.kt where it would live alongside other related methods. + // NOTE: Keep the implementation of this method in parity with StructProtoBuilder.putHeaders(). + fun Metadata.toStructProto(authUid: String?): Struct = buildStructProto { val keys: List> = run { val keySet: MutableSet = keys().toMutableSet() // Always explicitly include the auth header in the returned string, even if it is absent. @@ -119,7 +116,7 @@ internal class DataConnectGrpcMetadata( else { values.map { when (key.name()) { - firebaseAuthTokenHeader.name() -> it.toScrubbedAccessToken() + firebaseAuthTokenHeader.name() -> it.toScrubbedAccessToken() + " (authUid=$authUid)" firebaseAppCheckTokenHeader.name() -> it.toScrubbedAccessToken() else -> it } @@ -132,17 +129,45 @@ internal class DataConnectGrpcMetadata( } } + // TODO: Move this to ProtoUtil.kt where it would live alongside other related methods. + // NOTE: Keep the implementation of this method in parity with Metadata.toStructProto(). + fun StructProtoBuilder.putHeaders(key: String, headers: Map) { + putStruct(key) { + val keys: List = + buildSet { + addAll(headers.keys) + // Always explicitly include the auth header in the returned string, even if it is + // absent. + add(firebaseAuthTokenHeader.name()) + add(firebaseAppCheckTokenHeader.name()) + } + .sorted() + + keys.forEach { key -> + val value = headers[key] + val scrubbedValue = + value?.let { + when (key) { + firebaseAuthTokenHeader.name() -> it.toScrubbedAccessToken() + firebaseAppCheckTokenHeader.name() -> it.toScrubbedAccessToken() + else -> it + } + } + + put(key, scrubbedValue) + } + } + } + private val firebaseAuthTokenHeader: Metadata.Key = Metadata.Key.of("x-firebase-auth-token", Metadata.ASCII_STRING_MARSHALLER) private val firebaseAppCheckTokenHeader: Metadata.Key = Metadata.Key.of("x-firebase-appcheck", Metadata.ASCII_STRING_MARSHALLER) - @Suppress("SpellCheckingInspection") private val googRequestParamsHeader: Metadata.Key = Metadata.Key.of("x-goog-request-params", Metadata.ASCII_STRING_MARSHALLER) - @Suppress("SpellCheckingInspection") private val googApiClientHeader: Metadata.Key = Metadata.Key.of("x-goog-api-client", Metadata.ASCII_STRING_MARSHALLER) @@ -152,14 +177,10 @@ internal class DataConnectGrpcMetadata( fun forSystemVersions( firebaseApp: FirebaseApp, - dataConnectAuth: DataConnectAuth, - dataConnectAppCheck: DataConnectAppCheck, connectorLocation: String, parentLogger: Logger, ): DataConnectGrpcMetadata = DataConnectGrpcMetadata( - dataConnectAuth = dataConnectAuth, - dataConnectAppCheck = dataConnectAppCheck, connectorLocation = connectorLocation, kotlinVersion = "${KotlinVersion.CURRENT}", androidVersion = Build.VERSION.SDK_INT, diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCs.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCs.kt index a4599e230c0..1ba14f66650 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCs.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCs.kt @@ -187,8 +187,10 @@ internal class DataConnectGrpcRPCs( requestId: String, request: ExecuteMutationRequest, callerSdkType: FirebaseDataConnect.CallerSdkType, + authToken: DataConnectAuth.GetAuthTokenResult?, + appCheckToken: DataConnectAppCheck.GetAppCheckTokenResult?, ): ExecuteMutationResponse { - val metadata = grpcMetadata.get(requestId, callerSdkType).metadata + val metadata = grpcMetadata.get(authToken, appCheckToken, callerSdkType) val kotlinMethodName = "executeMutation(${request.operationName})" logger.logGrpcSending( @@ -198,6 +200,7 @@ internal class DataConnectGrpcRPCs( metadata = metadata, request = request.toStructProto(), requestTypeName = "ExecuteMutationRequest", + authUid = authToken?.authUid, ) val result = lazyGrpcStub.get().runCatching { executeMutation(request, metadata) } @@ -252,15 +255,10 @@ internal class DataConnectGrpcRPCs( request: ExecuteQueryRequest, callerSdkType: FirebaseDataConnect.CallerSdkType, fetchPolicy: FetchPolicy, + authToken: DataConnectAuth.GetAuthTokenResult?, + appCheckToken: DataConnectAppCheck.GetAppCheckTokenResult?, ): ExecuteQueryResult { - require( - fetchPolicy == FetchPolicy.PREFER_CACHE || - fetchPolicy == FetchPolicy.CACHE_ONLY || - fetchPolicy == FetchPolicy.SERVER_ONLY - ) { - "Only PREFER_CACHE, CACHE_ONLY, and SERVER_ONLY are supported for now" - } - val (metadata, authToken) = grpcMetadata.get(requestId, callerSdkType) + val metadata = grpcMetadata.get(authToken, appCheckToken, callerSdkType) val kotlinMethodName = "executeQuery(${request.operationName})" logger.logGrpcSending( @@ -270,6 +268,7 @@ internal class DataConnectGrpcRPCs( metadata = metadata, request = request.toStructProto(), requestTypeName = "ExecuteQueryRequest", + authUid = authToken?.authUid, ) val cacheInfo = queryCacheInfo(authToken, request) @@ -478,11 +477,12 @@ internal class DataConnectGrpcRPCs( grpcMethod: MethodDescriptor<*, *>, metadata: Metadata, request: Struct, - requestTypeName: String + requestTypeName: String, + authUid: String?, ) = debug { val struct = buildStructProto { put("RPC", grpcMethod.fullMethodName) - put("Metadata", metadata.toStructProto()) + put("Metadata", metadata.toStructProto(authUid)) put(requestTypeName, request) } // Sort the keys in the output string to be more meaningful than alphabetical. diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt index 0cd384cfd20..c54d0dc1931 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt @@ -258,8 +258,6 @@ internal class FirebaseDataConnectImpl( val grpcMetadata = DataConnectGrpcMetadata.forSystemVersions( firebaseApp = app, - dataConnectAuth = dataConnectAuth, - dataConnectAppCheck = dataConnectAppCheck, connectorLocation = config.location, parentLogger = logger, ) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt index af543389bf9..75017a20579 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt @@ -88,7 +88,7 @@ class DataConnectAuthUnitTest { @get:Rule val dataConnectLogLevelRule = DataConnectLogLevelRule() private val rs = RandomSource.default() - private val accessTokenGenerator = Arb.dataConnect.accessToken() + private val accessTokenGenerator = Arb.dataConnect.authToken() private val accessToken: String = accessTokenGenerator.next(rs) private val requestId = Arb.dataConnect.requestId().next(rs) private val mockInternalAuthProvider: InternalAuthProvider = diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt index 8b0bd30c92f..a4823722c3a 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt @@ -24,17 +24,24 @@ import com.google.firebase.dataconnect.DataConnectUntypedData import com.google.firebase.dataconnect.DataSource import com.google.firebase.dataconnect.FirebaseDataConnect.CallerSdkType import com.google.firebase.dataconnect.QueryRef.FetchPolicy +import com.google.firebase.dataconnect.core.DataConnectAppCheck.GetAppCheckTokenResult +import com.google.firebase.dataconnect.core.DataConnectAuth.GetAuthTokenResult import com.google.firebase.dataconnect.core.DataConnectGrpcClient.OperationResult import com.google.firebase.dataconnect.core.DataConnectGrpcClientGlobals.deserialize import com.google.firebase.dataconnect.core.DataConnectOperationFailureResponseImpl.ErrorInfoImpl import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.RandomSeedTestRule import com.google.firebase.dataconnect.testutil.newMockLogger +import com.google.firebase.dataconnect.testutil.property.arbitrary.TwoValues +import com.google.firebase.dataconnect.testutil.property.arbitrary.appCheckTokenResult +import com.google.firebase.dataconnect.testutil.property.arbitrary.authTokenResult import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect import com.google.firebase.dataconnect.testutil.property.arbitrary.iterator import com.google.firebase.dataconnect.testutil.property.arbitrary.operationErrors +import com.google.firebase.dataconnect.testutil.property.arbitrary.pair import com.google.firebase.dataconnect.testutil.property.arbitrary.proto import com.google.firebase.dataconnect.testutil.property.arbitrary.struct +import com.google.firebase.dataconnect.testutil.registerDataConnectKotestPrinters import com.google.firebase.dataconnect.testutil.shouldHaveLoggedExactlyOneMessageContaining import com.google.firebase.dataconnect.testutil.shouldSatisfy import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto @@ -74,6 +81,7 @@ import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.merge import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.orNull import io.kotest.property.arbitrary.string import io.kotest.property.assume import io.kotest.property.checkAll @@ -84,7 +92,8 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.spyk import io.mockk.verify -import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import kotlin.random.Random import kotlin.reflect.KClass import kotlinx.coroutines.test.runTest import kotlinx.serialization.DeserializationStrategy @@ -93,6 +102,7 @@ import kotlinx.serialization.SerializationException import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.serializer +import org.junit.Before import org.junit.Rule import org.junit.Test @@ -114,15 +124,19 @@ class DataConnectGrpcClientUnitTest { private val fetchPolicy = Arb.enum().next(rs) private val mockDataConnectAuth: DataConnectAuth = - mockk(relaxed = true, name = "mockDataConnectAuth-zfbhma6tyh") + mockk(relaxed = true, name = "mockDataConnectAuth-zfbhma6tyh") { + coEvery { getToken(any()) } returns null + } private val mockDataConnectAppCheck: DataConnectAppCheck = - mockk(relaxed = true, name = "mockDataConnectAppCheck-zfbhma6tyh") + mockk(relaxed = true, name = "mockDataConnectAppCheck-zfbhma6tyh") { + coEvery { getToken(any()) } returns null + } private val mockDataConnectGrpcRPCs: DataConnectGrpcRPCs = mockk(relaxed = true, name = "mockDataConnectGrpcRPCs-zfbhma6tyh") { - coEvery { executeQuery(any(), any(), any(), any()) } returns + coEvery { executeQuery(any(), any(), any(), any(), any(), any()) } returns DataConnectGrpcRPCs.ExecuteQueryResult.FromServer(ExecuteQueryResponse.getDefaultInstance()) - coEvery { executeMutation(any(), any(), any()) } returns + coEvery { executeMutation(any(), any(), any(), any(), any()) } returns ExecuteMutationResponse.getDefaultInstance() } @@ -138,6 +152,11 @@ class DataConnectGrpcClientUnitTest { logger = mockLogger, ) + @Before + fun registerPrinters() { + registerDataConnectKotestPrinters() + } + @Test fun `executeQuery() should forward requestId, callerSdkType, and fetchPolicy`() = runTest { checkAll( @@ -154,7 +173,14 @@ class DataConnectGrpcClientUnitTest { fetchPolicy ) coVerify { - mockDataConnectGrpcRPCs.executeQuery(requestId, any(), callerSdkType, fetchPolicy) + mockDataConnectGrpcRPCs.executeQuery( + requestId, + any(), + callerSdkType, + fetchPolicy, + any(), + any() + ) } } } @@ -181,7 +207,9 @@ class DataConnectGrpcClientUnitTest { .setOperationName(operationName) .setVariables(variables) .build() - coVerify { mockDataConnectGrpcRPCs.executeQuery(any(), expectedRequest, any(), any()) } + coVerify { + mockDataConnectGrpcRPCs.executeQuery(any(), expectedRequest, any(), any(), any(), any()) + } } } @@ -192,7 +220,9 @@ class DataConnectGrpcClientUnitTest { callerSdkType -> dataConnectGrpcClient.executeMutation(requestId, operationName, variables, callerSdkType) - coVerify { mockDataConnectGrpcRPCs.executeMutation(requestId, any(), callerSdkType) } + coVerify { + mockDataConnectGrpcRPCs.executeMutation(requestId, any(), callerSdkType, any(), any()) + } } } @@ -211,14 +241,17 @@ class DataConnectGrpcClientUnitTest { .setOperationName(operationName) .setVariables(variables) .build() - coVerify { mockDataConnectGrpcRPCs.executeMutation(any(), expectedRequest, any()) } + coVerify { + mockDataConnectGrpcRPCs.executeMutation(any(), expectedRequest, any(), any(), any()) + } } @Test fun `executeQuery() should return data and empty errors if response is from cache`() = runTest { val responseData = Arb.proto.struct().next(rs).struct - coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } returns - DataConnectGrpcRPCs.ExecuteQueryResult.FromCache(responseData) + coEvery { + mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any(), any(), any()) + } returns DataConnectGrpcRPCs.ExecuteQueryResult.FromCache(responseData) val operationResult = dataConnectGrpcClient.executeQuery( @@ -235,7 +268,9 @@ class DataConnectGrpcClientUnitTest { @Test fun `executeQuery() should return null data and empty errors if response is empty`() = runTest { - coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } returns + coEvery { + mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any(), any(), any()) + } returns DataConnectGrpcRPCs.ExecuteQueryResult.FromServer(ExecuteQueryResponse.getDefaultInstance()) val operationResult = @@ -253,7 +288,7 @@ class DataConnectGrpcClientUnitTest { @Test fun `executeMutation() should return null data and empty errors if response is empty`() = runTest { - coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } returns + coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any(), any(), any()) } returns ExecuteMutationResponse.getDefaultInstance() val operationResult = @@ -266,7 +301,9 @@ class DataConnectGrpcClientUnitTest { fun `executeQuery() should return data and errors`() = runTest { val responseData = Arb.proto.struct().next(rs).struct val responseErrors = List(3) { GraphqlErrorInfo.random(RandomSource.default()) } - coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } returns + coEvery { + mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any(), any(), any()) + } returns DataConnectGrpcRPCs.ExecuteQueryResult.FromServer( ExecuteQueryResponse.newBuilder() .setData(responseData) @@ -295,7 +332,7 @@ class DataConnectGrpcClientUnitTest { fun `executeMutation() should return data and errors`() = runTest { val responseData = Arb.proto.struct().next(rs).struct val responseErrors = List(3) { GraphqlErrorInfo.random(RandomSource.default()) } - coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } returns + coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any(), any(), any()) } returns ExecuteMutationResponse.newBuilder() .setData(responseData) .addAllErrors(responseErrors.map { it.graphqlError }) @@ -315,7 +352,9 @@ class DataConnectGrpcClientUnitTest { @Test fun `executeQuery() should propagate non-grpc exceptions`() = runTest { val exception = TestException("k6hzgp7hvz") - coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } throws exception + coEvery { + mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any(), any(), any()) + } throws exception val thrownException = shouldThrow { @@ -334,7 +373,8 @@ class DataConnectGrpcClientUnitTest { @Test fun `executeMutation() should propagate non-grpc exceptions`() = runTest { val exception = TestException("g32376rnd3") - coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } throws exception + coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any(), any(), any()) } throws + exception val thrownException = shouldThrow { @@ -345,149 +385,108 @@ class DataConnectGrpcClientUnitTest { } @Test - fun `executeQuery() should retry with a fresh auth token on UNAUTHENTICATED`() = runTest { - val responseData = Arb.proto.struct().next(rs).struct - val forceRefresh = AtomicBoolean(false) - coEvery { mockDataConnectAuth.forceRefresh() } answers { forceRefresh.set(true) } - coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } answers - { - if (forceRefresh.get()) { - DataConnectGrpcRPCs.ExecuteQueryResult.FromServer( - ExecuteQueryResponse.newBuilder().setData(responseData).build() - ) - } else { - // Use a custom description to ensure that DataConnectGrpcClient is checking for just - // the code, and not the entire equality of Status.UNAUTHENTICATED. - throw StatusException( - Status.UNAUTHENTICATED.withDescription( - "this error should be ignored and result in a retry with a fresh token n2ak4cq6jr" - ) - ) - } - } - - val result = - dataConnectGrpcClient.executeQuery( - requestId, - operationName, - variables, - callerSdkType, - fetchPolicy - ) - - result shouldBe OperationResult(data = responseData, errors = emptyList(), DataSource.SERVER) - coVerify(exactly = 2) { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } - mockLogger.shouldHaveLoggedExactlyOneMessageContaining( - "retrying with fresh Auth and/or AppCheck tokens" - ) - mockLogger.shouldHaveLoggedExactlyOneMessageContaining("UNAUTHENTICATED") - } - - @Test - fun `executeMutation() should retry with a fresh auth token on UNAUTHENTICATED`() = runTest { - val responseData = Arb.proto.struct().next(rs).struct - val forceRefresh = AtomicBoolean(false) - coEvery { mockDataConnectAuth.forceRefresh() } answers { forceRefresh.set(true) } - coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } answers - { - if (forceRefresh.get()) { - ExecuteMutationResponse.newBuilder().setData(responseData).build() - } else { - // Use a custom description to ensure that DataConnectGrpcClient is checking for just - // the code, and not the entire equality of Status.UNAUTHENTICATED. - throw StatusException( - Status.UNAUTHENTICATED.withDescription( - "this error should be ignored and result in a retry with a fresh token p3vmc3gs5v" - ) - ) - } - } - - val result = - dataConnectGrpcClient.executeMutation(requestId, operationName, variables, callerSdkType) + fun `executeQuery() should retry with fresh auth and app check tokens on UNAUTHENTICATED`() = + runTest { + val authTokens = authTokenPairArb().next(rs) + mockDataConnectAuth.stubGetTokensSimulatingForceRefresh(authTokens) + val appCheckTokens = appCheckTokenPairArb().next(rs) + mockDataConnectAppCheck.stubGetTokensSimulatingForceRefresh(appCheckTokens) + coEvery { + mockDataConnectGrpcRPCs.executeQuery( + any(), + any(), + any(), + any(), + matchNullable { it == authTokens.value1 }, + matchNullable { it == appCheckTokens.value1 }, + ) + } throws + StatusException( + Status.UNAUTHENTICATED.withDescription("status exception ${Random.nextInt()} bjh9zc6h5n") + ) + val responseData = Arb.proto.struct().next(rs).struct + coEvery { + mockDataConnectGrpcRPCs.executeQuery( + any(), + any(), + any(), + any(), + matchNullable { it == authTokens.value2 }, + matchNullable { it == appCheckTokens.value2 }, + ) + } returns + DataConnectGrpcRPCs.ExecuteQueryResult.FromServer( + ExecuteQueryResponse.newBuilder().setData(responseData).build() + ) - result shouldBe OperationResult(data = responseData, errors = emptyList(), DataSource.SERVER) - coVerify(exactly = 2) { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } - mockLogger.shouldHaveLoggedExactlyOneMessageContaining( - "retrying with fresh Auth and/or AppCheck tokens" - ) - mockLogger.shouldHaveLoggedExactlyOneMessageContaining("UNAUTHENTICATED") - } + val result = + dataConnectGrpcClient.executeQuery( + requestId, + operationName, + variables, + callerSdkType, + fetchPolicy + ) - @Test - fun `executeQuery() should retry with a fresh AppCheck token on UNAUTHENTICATED`() = runTest { - val responseData = Arb.proto.struct().next(rs).struct - val forceRefresh = AtomicBoolean(false) - coEvery { mockDataConnectAppCheck.forceRefresh() } answers { forceRefresh.set(true) } - coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } answers - { - if (forceRefresh.get()) { - DataConnectGrpcRPCs.ExecuteQueryResult.FromServer( - ExecuteQueryResponse.newBuilder().setData(responseData).build() - ) - } else { - // Use a custom description to ensure that DataConnectGrpcClient is checking for just - // the code, and not the entire equality of Status.UNAUTHENTICATED. - throw StatusException( - Status.UNAUTHENTICATED.withDescription( - "this error should be ignored and result in a retry with a fresh token tepb5xq4kk" - ) - ) - } + result shouldBe OperationResult(data = responseData, errors = emptyList(), DataSource.SERVER) + coVerify(exactly = 2) { + mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any(), any(), any()) } - - val result = - dataConnectGrpcClient.executeQuery( - requestId, - operationName, - variables, - callerSdkType, - fetchPolicy + mockLogger.shouldHaveLoggedExactlyOneMessageContaining( + "retrying with fresh Auth and/or AppCheck tokens" ) - - result shouldBe OperationResult(data = responseData, errors = emptyList(), DataSource.SERVER) - coVerify(exactly = 2) { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } - mockLogger.shouldHaveLoggedExactlyOneMessageContaining( - "retrying with fresh Auth and/or AppCheck tokens" - ) - mockLogger.shouldHaveLoggedExactlyOneMessageContaining("UNAUTHENTICATED") - } + mockLogger.shouldHaveLoggedExactlyOneMessageContaining("UNAUTHENTICATED") + } @Test - fun `executeMutation() should retry with a fresh AppCheck token on UNAUTHENTICATED`() = runTest { - val responseData = Arb.proto.struct().next(rs).struct - val forceRefresh = AtomicBoolean(false) - coEvery { mockDataConnectAppCheck.forceRefresh() } answers { forceRefresh.set(true) } - coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } answers - { - if (forceRefresh.get()) { - ExecuteMutationResponse.newBuilder().setData(responseData).build() - } else { - // Use a custom description to ensure that DataConnectGrpcClient is checking for just - // the code, and not the entire equality of Status.UNAUTHENTICATED. - throw StatusException( - Status.UNAUTHENTICATED.withDescription( - "this error should be ignored and result in a retry with a fresh token v2449h6ty8" - ) - ) - } - } + fun `executeMutation() should retry with fresh auth and app check tokens on UNAUTHENTICATED`() = + runTest { + val authTokens = authTokenPairArb().next(rs) + mockDataConnectAuth.stubGetTokensSimulatingForceRefresh(authTokens) + val appCheckTokens = appCheckTokenPairArb().next(rs) + mockDataConnectAppCheck.stubGetTokensSimulatingForceRefresh(appCheckTokens) + coEvery { + mockDataConnectGrpcRPCs.executeMutation( + any(), + any(), + any(), + matchNullable { it == authTokens.value1 }, + matchNullable { it == appCheckTokens.value1 }, + ) + } throws + StatusException( + Status.UNAUTHENTICATED.withDescription("status exception ${Random.nextInt()} m8gzej7pmy") + ) + val responseData = Arb.proto.struct().next(rs).struct + coEvery { + mockDataConnectGrpcRPCs.executeMutation( + any(), + any(), + any(), + matchNullable { it == authTokens.value2 }, + matchNullable { it == appCheckTokens.value2 }, + ) + } returns ExecuteMutationResponse.newBuilder().setData(responseData).build() - val result = - dataConnectGrpcClient.executeMutation(requestId, operationName, variables, callerSdkType) + val result = + dataConnectGrpcClient.executeMutation(requestId, operationName, variables, callerSdkType) - result shouldBe OperationResult(data = responseData, errors = emptyList(), DataSource.SERVER) - coVerify(exactly = 2) { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } - mockLogger.shouldHaveLoggedExactlyOneMessageContaining( - "retrying with fresh Auth and/or AppCheck tokens" - ) - mockLogger.shouldHaveLoggedExactlyOneMessageContaining("UNAUTHENTICATED") - } + result shouldBe OperationResult(data = responseData, errors = emptyList(), DataSource.SERVER) + coVerify(exactly = 2) { + mockDataConnectGrpcRPCs.executeMutation(any(), any(), any(), any(), any()) + } + mockLogger.shouldHaveLoggedExactlyOneMessageContaining( + "retrying with fresh Auth and/or AppCheck tokens" + ) + mockLogger.shouldHaveLoggedExactlyOneMessageContaining("UNAUTHENTICATED") + } @Test fun `executeQuery() should NOT retry on error status other than UNAUTHENTICATED`() = runTest { val exception = StatusException(Status.INTERNAL) - coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } throws exception + coEvery { + mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any(), any(), any()) + } throws exception val thrownException = shouldThrow { @@ -508,7 +507,8 @@ class DataConnectGrpcClientUnitTest { @Test fun `executeMutation() should NOT retry on error status other than UNAUTHENTICATED`() = runTest { val exception = StatusException(Status.INTERNAL) - coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } throws exception + coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any(), any(), any()) } throws + exception val thrownException = shouldThrow { @@ -525,8 +525,9 @@ class DataConnectGrpcClientUnitTest { runTest { val exception1 = StatusException(Status.UNAUTHENTICATED) val exception2 = StatusException(Status.UNAUTHENTICATED) - coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } throwsMany - (listOf(exception1, exception2)) + coEvery { + mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any(), any(), any()) + } throwsMany (listOf(exception1, exception2)) val thrownException = shouldThrow { @@ -547,8 +548,9 @@ class DataConnectGrpcClientUnitTest { runTest { val exception1 = StatusException(Status.UNAUTHENTICATED) val exception2 = StatusException(Status.UNAUTHENTICATED) - coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } throwsMany - (listOf(exception1, exception2)) + coEvery { + mockDataConnectGrpcRPCs.executeMutation(any(), any(), any(), any(), any()) + } throwsMany (listOf(exception1, exception2)) val thrownException = shouldThrow { @@ -563,8 +565,9 @@ class DataConnectGrpcClientUnitTest { runTest { val exception1 = StatusException(Status.UNAUTHENTICATED) val exception2 = StatusException(Status.ABORTED) - coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } throwsMany - (listOf(exception1, exception2)) + coEvery { + mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any(), any(), any()) + } throwsMany (listOf(exception1, exception2)) val thrownException = shouldThrow { @@ -585,8 +588,9 @@ class DataConnectGrpcClientUnitTest { runTest { val exception1 = StatusException(Status.UNAUTHENTICATED) val exception2 = StatusException(Status.ABORTED) - coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } throwsMany - (listOf(exception1, exception2)) + coEvery { + mockDataConnectGrpcRPCs.executeMutation(any(), any(), any(), any(), any()) + } throwsMany (listOf(exception1, exception2)) val thrownException = shouldThrow { @@ -601,8 +605,9 @@ class DataConnectGrpcClientUnitTest { runTest { val exception1 = StatusException(Status.UNAUTHENTICATED) val exception2 = TestException("eysrmxmxk7") - coEvery { mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any()) } throwsMany - (listOf(exception1, exception2)) + coEvery { + mockDataConnectGrpcRPCs.executeQuery(any(), any(), any(), any(), any(), any()) + } throwsMany (listOf(exception1, exception2)) val thrownException = shouldThrow { @@ -623,8 +628,9 @@ class DataConnectGrpcClientUnitTest { runTest { val exception1 = StatusException(Status.UNAUTHENTICATED) val exception2 = TestException("qz2ykb8wa2") - coEvery { mockDataConnectGrpcRPCs.executeMutation(any(), any(), any()) } throwsMany - (listOf(exception1, exception2)) + coEvery { + mockDataConnectGrpcRPCs.executeMutation(any(), any(), any(), any(), any()) + } throwsMany (listOf(exception1, exception2)) val thrownException = shouldThrow { @@ -908,3 +914,27 @@ class DataConnectGrpcClientOperationResultUnitTest { } } } + +private fun DataConnectAuth.stubGetTokensSimulatingForceRefresh( + tokens: Iterable +) { + val tokenIterator = tokens.iterator() + val currentToken = AtomicReference(tokenIterator.next()) + coEvery { getToken(any()) } answers { currentToken.get() } + coEvery { forceRefresh() } answers { currentToken.set(tokenIterator.next()) } +} + +private fun DataConnectAppCheck.stubGetTokensSimulatingForceRefresh( + tokens: Iterable +) { + val tokenIterator = tokens.iterator() + val currentToken = AtomicReference(tokenIterator.next()) + coEvery { getToken(any()) } answers { currentToken.get() } + coEvery { forceRefresh() } answers { currentToken.set(tokenIterator.next()) } +} + +private fun authTokenPairArb(): Arb> = + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.33).pair() + +private fun appCheckTokenPairArb(): Arb> = + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.33).pair() diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadataUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadataUnitTest.kt index 1bde35303f7..5a4a4fe36d4 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadataUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadataUnitTest.kt @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalKotest::class) + package com.google.firebase.dataconnect.core import android.os.Build import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.firebase.dataconnect.BuildConfig import com.google.firebase.dataconnect.FirebaseDataConnect.CallerSdkType -import com.google.firebase.dataconnect.core.DataConnectAppCheck.GetAppCheckTokenResult -import com.google.firebase.dataconnect.core.DataConnectAuth.GetAuthTokenResult import com.google.firebase.dataconnect.testutil.FirebaseAppUnitTestingRule import com.google.firebase.dataconnect.testutil.property.arbitrary.appCheckTokenResult import com.google.firebase.dataconnect.testutil.property.arbitrary.authTokenResult @@ -28,19 +28,20 @@ import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnectGrpcMetadata import io.grpc.Metadata import io.kotest.assertions.asClue -import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldNotContain import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeSameInstanceAs import io.kotest.property.Arb -import io.kotest.property.Exhaustive +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.ShrinkingMode import io.kotest.property.arbitrary.constant import io.kotest.property.arbitrary.enum import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.orNull import io.kotest.property.checkAll -import io.kotest.property.exhaustive.of -import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -60,226 +61,310 @@ class DataConnectGrpcMetadataUnitTest { @Test fun `should include x-goog-api-client when callerSdkType is Generated`() = runTest { - val dataConnectGrpcMetadata = - Arb.dataConnect - .dataConnectGrpcMetadata( - kotlinVersion = Arb.constant("cdsz85awyc"), - androidVersion = Arb.constant(490843892), - dataConnectSdkVersion = Arb.constant("v3q46qc2ax"), - grpcVersion = Arb.constant("fq9fhx6j5e"), - ) - .next() - val requestId = Arb.dataConnect.requestId().next() + val dataConnectGrpcMetadataArb = + Arb.dataConnect.dataConnectGrpcMetadata( + kotlinVersion = Arb.constant("cdsz85awyc"), + androidVersion = Arb.constant(490843892), + dataConnectSdkVersion = Arb.constant("v3q46qc2ax"), + grpcVersion = Arb.constant("fq9fhx6j5e"), + ) - val metadata = - dataConnectGrpcMetadata.get(requestId, callerSdkType = CallerSdkType.Generated).metadata + checkAll( + propTestConfig, + dataConnectGrpcMetadataArb, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.33), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.33), + ) { dataConnectGrpcMetadata, authToken, appCheckToken -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = appCheckToken, + callerSdkType = CallerSdkType.Generated, + ) - metadata.asClue { - it.keys() shouldContain "x-goog-api-client" - val metadataKey = Metadata.Key.of("x-goog-api-client", Metadata.ASCII_STRING_MARSHALLER) - it.get(metadataKey) shouldBe - "gl-kotlin/cdsz85awyc gl-android/490843892 fire/v3q46qc2ax grpc/fq9fhx6j5e kotlin/gen" + metadata.asClue { + it.keys() shouldContain "x-goog-api-client" + val metadataKey = Metadata.Key.of("x-goog-api-client", Metadata.ASCII_STRING_MARSHALLER) + it.get(metadataKey) shouldBe + "gl-kotlin/cdsz85awyc gl-android/490843892 fire/v3q46qc2ax grpc/fq9fhx6j5e kotlin/gen" + } } } @Test fun `should include x-goog-api-client when callerSdkType is Base`() = runTest { - val dataConnectGrpcMetadata = - Arb.dataConnect - .dataConnectGrpcMetadata( - kotlinVersion = Arb.constant("cdsz85awyc"), - androidVersion = Arb.constant(490843892), - dataConnectSdkVersion = Arb.constant("v3q46qc2ax"), - grpcVersion = Arb.constant("fq9fhx6j5e"), - ) - .next() - val requestId = Arb.dataConnect.requestId().next() + val dataConnectGrpcMetadataArb = + Arb.dataConnect.dataConnectGrpcMetadata( + kotlinVersion = Arb.constant("cdsz85awyc"), + androidVersion = Arb.constant(490843892), + dataConnectSdkVersion = Arb.constant("v3q46qc2ax"), + grpcVersion = Arb.constant("fq9fhx6j5e"), + ) - val metadata = - dataConnectGrpcMetadata.get(requestId, callerSdkType = CallerSdkType.Base).metadata + checkAll( + propTestConfig, + dataConnectGrpcMetadataArb, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.33), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.33), + ) { dataConnectGrpcMetadata, authToken, appCheckToken -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = appCheckToken, + callerSdkType = CallerSdkType.Base, + ) - metadata.asClue { - it.keys() shouldContain "x-goog-api-client" - val metadataKey = Metadata.Key.of("x-goog-api-client", Metadata.ASCII_STRING_MARSHALLER) - it.get(metadataKey) shouldBe - "gl-kotlin/cdsz85awyc gl-android/490843892 fire/v3q46qc2ax grpc/fq9fhx6j5e" + metadata.asClue { + it.keys() shouldContain "x-goog-api-client" + val metadataKey = Metadata.Key.of("x-goog-api-client", Metadata.ASCII_STRING_MARSHALLER) + it.get(metadataKey) shouldBe + "gl-kotlin/cdsz85awyc gl-android/490843892 fire/v3q46qc2ax grpc/fq9fhx6j5e" + } } } @Test fun `should include x-goog-request-params`() = runTest { - val dataConnectGrpcMetadata = - Arb.dataConnect - .dataConnectGrpcMetadata( - connectorLocation = Arb.constant("q8mgtztcz2"), - ) - .next() - val requestId = Arb.dataConnect.requestId().next() - val callerSdkType = Arb.enum().next() + val dataConnectGrpcMetadataArb = + Arb.dataConnect.dataConnectGrpcMetadata( + connectorLocation = Arb.constant("q8mgtztcz2"), + ) - val metadata = dataConnectGrpcMetadata.get(requestId, callerSdkType).metadata + checkAll( + propTestConfig, + dataConnectGrpcMetadataArb, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.33), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.33), + Arb.enum() + ) { dataConnectGrpcMetadata, authToken, appCheckToken, callerSdkType -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = appCheckToken, + callerSdkType = callerSdkType, + ) - metadata.asClue { - it.keys() shouldContain "x-goog-request-params" - val metadataKey = Metadata.Key.of("x-goog-request-params", Metadata.ASCII_STRING_MARSHALLER) - it.get(metadataKey) shouldBe "location=q8mgtztcz2&frontend=data" + metadata.asClue { + it.keys() shouldContain "x-goog-request-params" + val metadataKey = Metadata.Key.of("x-goog-request-params", Metadata.ASCII_STRING_MARSHALLER) + it.get(metadataKey) shouldBe "location=q8mgtztcz2&frontend=data" + } } } @Test fun `should include x-firebase-gmpid`() = runTest { - val dataConnectGrpcMetadata = - Arb.dataConnect.dataConnectGrpcMetadata(appId = Arb.constant("tvsxjeb745.appId")).next() - val requestId = Arb.dataConnect.requestId().next() - val callerSdkType = Arb.enum().next() - - val metadata = dataConnectGrpcMetadata.get(requestId, callerSdkType).metadata + val dataConnectGrpcMetadataArb = + Arb.dataConnect.dataConnectGrpcMetadata(appId = Arb.constant("tvsxjeb745.appId")) + + checkAll( + propTestConfig, + dataConnectGrpcMetadataArb, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.33), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.33), + Arb.enum() + ) { dataConnectGrpcMetadata, authToken, appCheckToken, callerSdkType -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = appCheckToken, + callerSdkType = callerSdkType, + ) - metadata.asClue { - it.keys() shouldContain "x-firebase-gmpid" - val metadataKey = Metadata.Key.of("x-firebase-gmpid", Metadata.ASCII_STRING_MARSHALLER) - it.get(metadataKey) shouldBe "tvsxjeb745.appId" + metadata.asClue { + it.keys() shouldContain "x-firebase-gmpid" + val metadataKey = Metadata.Key.of("x-firebase-gmpid", Metadata.ASCII_STRING_MARSHALLER) + it.get(metadataKey) shouldBe "tvsxjeb745.appId" + } } } @Test fun `should NOT include x-firebase-gmpid if appId is the empty string`() = runTest { - val dataConnectGrpcMetadata = - Arb.dataConnect.dataConnectGrpcMetadata(appId = Arb.constant("")).next() - val requestId = Arb.dataConnect.requestId().next() - val callerSdkType = Arb.enum().next() - - val metadata = dataConnectGrpcMetadata.get(requestId, callerSdkType).metadata + val dataConnectGrpcMetadataArb = + Arb.dataConnect.dataConnectGrpcMetadata(appId = Arb.constant("")) + + checkAll( + propTestConfig, + dataConnectGrpcMetadataArb, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.33), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.33), + Arb.enum() + ) { dataConnectGrpcMetadata, authToken, appCheckToken, callerSdkType -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = appCheckToken, + callerSdkType = callerSdkType, + ) - metadata.asClue { it.keys() shouldNotContain "x-firebase-gmpid" } + metadata.asClue { it.keys() shouldNotContain "x-firebase-gmpid" } + } } @Test fun `should NOT include x-firebase-gmpid if appId is blank`() = runTest { - val dataConnectGrpcMetadata = - Arb.dataConnect.dataConnectGrpcMetadata(appId = Arb.constant(" \r\n\t ")).next() - val requestId = Arb.dataConnect.requestId().next() - val callerSdkType = Arb.enum().next() - - val metadata = dataConnectGrpcMetadata.get(requestId, callerSdkType).metadata + val dataConnectGrpcMetadataArb = + Arb.dataConnect.dataConnectGrpcMetadata(appId = Arb.constant(" \r\n\t ")) + + checkAll( + propTestConfig, + dataConnectGrpcMetadataArb, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.33), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.33), + Arb.enum() + ) { dataConnectGrpcMetadata, authToken, appCheckToken, callerSdkType -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = appCheckToken, + callerSdkType = callerSdkType, + ) - metadata.asClue { it.keys() shouldNotContain "x-firebase-gmpid" } + metadata.asClue { it.keys() shouldNotContain "x-firebase-gmpid" } + } } @Test - fun `should omit x-firebase-auth-token when the auth token is null`() = runTest { - val getAuthTokenResults: Exhaustive = - Exhaustive.of( - null, - Arb.dataConnect.authTokenResult(accessToken = Arb.constant(null)).next(), - ) - - checkAll(getAuthTokenResults) { getAuthTokenResult -> - val dataConnectAuth: DataConnectAuth = mockk() - coEvery { dataConnectAuth.getToken(any()) } returns getAuthTokenResult - val dataConnectGrpcMetadata = - Arb.dataConnect - .dataConnectGrpcMetadata(dataConnectAuth = Arb.constant(dataConnectAuth)) - .next() - val requestId = Arb.dataConnect.requestId().next() - val callerSdkType = Arb.enum().next() - - val (metadata, authToken) = dataConnectGrpcMetadata.get(requestId, callerSdkType) + fun `should omit x-firebase-auth-token when the auth token result is null`() = runTest { + checkAll( + propTestConfig, + Arb.dataConnect.dataConnectGrpcMetadata(), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.33), + Arb.enum() + ) { dataConnectGrpcMetadata, appCheckToken, callerSdkType -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = null, + appCheckToken = appCheckToken, + callerSdkType = callerSdkType, + ) metadata.asClue { it.keys() shouldNotContain "x-firebase-auth-token" } - withClue("authToken") { authToken shouldBe getAuthTokenResult } } } @Test - fun `should include x-firebase-auth-token when the auth token is not null`() = runTest { - val dataConnectAuth: DataConnectAuth = mockk() - val authTokenResult = - Arb.dataConnect.authTokenResult(accessToken = Arb.dataConnect.accessToken()).next() - coEvery { dataConnectAuth.getToken(any()) } returns authTokenResult - val dataConnectGrpcMetadata = - Arb.dataConnect - .dataConnectGrpcMetadata(dataConnectAuth = Arb.constant(dataConnectAuth)) - .next() - val requestId = Arb.dataConnect.requestId().next() - val callerSdkType = Arb.enum().next() - - val (metadata, authToken) = dataConnectGrpcMetadata.get(requestId, callerSdkType) - - metadata.asClue { - it.keys() shouldContain "x-firebase-auth-token" - val metadataKey = Metadata.Key.of("x-firebase-auth-token", Metadata.ASCII_STRING_MARSHALLER) - it.get(metadataKey) shouldBe authTokenResult.token - withClue("authToken") { authToken shouldBe authTokenResult } + fun `should omit x-firebase-auth-token when the auth token result is not null but the token is null`() = + runTest { + checkAll( + propTestConfig, + Arb.dataConnect.dataConnectGrpcMetadata(), + Arb.dataConnect.authTokenResult(accessToken = Arb.constant(null)), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.33), + Arb.enum() + ) { dataConnectGrpcMetadata, authToken, appCheckToken, callerSdkType -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = appCheckToken, + callerSdkType = callerSdkType, + ) + + metadata.asClue { it.keys() shouldNotContain "x-firebase-auth-token" } + } } - } @Test - fun `should omit x-firebase-appcheck when the AppCheck token is null`() = runTest { - val getAppCheckTokenResults: Exhaustive = - Exhaustive.of( - null, - Arb.dataConnect.appCheckTokenResult(accessToken = Arb.constant(null)).next(), - ) + fun `should include x-firebase-auth-token when the auth token is not null`() = runTest { + checkAll( + propTestConfig, + Arb.dataConnect.dataConnectGrpcMetadata(), + Arb.dataConnect.authTokenResult(accessToken = Arb.dataConnect.authToken()), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.33), + Arb.enum() + ) { dataConnectGrpcMetadata, authToken, appCheckToken, callerSdkType -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = appCheckToken, + callerSdkType = callerSdkType, + ) - checkAll(getAppCheckTokenResults) { getAppCheckTokenResult -> - val dataConnectAppCheck: DataConnectAppCheck = mockk { - coEvery { getToken(any()) } returns getAppCheckTokenResult + metadata.asClue { + it.keys() shouldContain "x-firebase-auth-token" + val metadataKey = Metadata.Key.of("x-firebase-auth-token", Metadata.ASCII_STRING_MARSHALLER) + it.get(metadataKey) shouldBe authToken.token } - val dataConnectGrpcMetadata = - Arb.dataConnect - .dataConnectGrpcMetadata(dataConnectAppCheck = Arb.constant(dataConnectAppCheck)) - .next() - val requestId = Arb.dataConnect.requestId().next() - val callerSdkType = Arb.enum().next() + } + } - val metadata = dataConnectGrpcMetadata.get(requestId, callerSdkType).metadata + @Test + fun `should omit x-firebase-appcheck when the AppCheck token result is null`() = runTest { + checkAll( + propTestConfig, + Arb.dataConnect.dataConnectGrpcMetadata(), + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.33), + Arb.enum() + ) { dataConnectGrpcMetadata, authToken, callerSdkType -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = null, + callerSdkType = callerSdkType, + ) metadata.asClue { it.keys() shouldNotContain "x-firebase-appcheck" } } } @Test - fun `should include x-firebase-appcheck when the AppCheck token is not null`() = runTest { - val appCheckTokenResult = - Arb.dataConnect.appCheckTokenResult(accessToken = Arb.dataConnect.accessToken()).next() - val dataConnectAppCheck: DataConnectAppCheck = mockk { - coEvery { getToken(any()) } returns appCheckTokenResult + fun `should omit x-firebase-appcheck when the AppCheck token result is not null but the token is null`() = + runTest { + checkAll( + propTestConfig, + Arb.dataConnect.dataConnectGrpcMetadata(), + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.33), + Arb.dataConnect.appCheckTokenResult(accessToken = Arb.constant(null)), + Arb.enum() + ) { dataConnectGrpcMetadata, authToken, appCheckToken, callerSdkType -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = appCheckToken, + callerSdkType = callerSdkType, + ) + + metadata.asClue { it.keys() shouldNotContain "x-firebase-appcheck" } + } } - val dataConnectGrpcMetadata = - Arb.dataConnect - .dataConnectGrpcMetadata(dataConnectAppCheck = Arb.constant(dataConnectAppCheck)) - .next() - val requestId = Arb.dataConnect.requestId().next() - val callerSdkType = Arb.enum().next() - - val metadata = dataConnectGrpcMetadata.get(requestId, callerSdkType).metadata - - metadata.asClue { - it.keys() shouldContain "x-firebase-appcheck" - val metadataKey = Metadata.Key.of("x-firebase-appcheck", Metadata.ASCII_STRING_MARSHALLER) - it.get(metadataKey) shouldBe appCheckTokenResult.token + + @Test + fun `should include x-firebase-appcheck when the AppCheck token is not null`() = runTest { + checkAll( + propTestConfig, + Arb.dataConnect.dataConnectGrpcMetadata(), + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.33), + Arb.dataConnect.appCheckTokenResult(accessToken = Arb.dataConnect.appCheckToken()), + Arb.enum() + ) { dataConnectGrpcMetadata, authToken, appCheckToken, callerSdkType -> + val metadata = + dataConnectGrpcMetadata.get( + authToken = authToken, + appCheckToken = appCheckToken, + callerSdkType = callerSdkType, + ) + + metadata.asClue { + it.keys() shouldContain "x-firebase-appcheck" + val metadataKey = Metadata.Key.of("x-firebase-appcheck", Metadata.ASCII_STRING_MARSHALLER) + it.get(metadataKey) shouldBe appCheckToken.token + } } } @Test fun `forSystemVersions() should return correct values`() = runTest { - val dataConnectAuth: DataConnectAuth = mockk() - val dataConnectAppCheck: DataConnectAppCheck = mockk() val connectorLocation = Arb.dataConnect.connectorLocation().next() val dataConnectGrpcMetadata = DataConnectGrpcMetadata.forSystemVersions( firebaseApp = firebaseAppFactory.newInstance(), - dataConnectAuth = dataConnectAuth, - dataConnectAppCheck = dataConnectAppCheck, connectorLocation = connectorLocation, parentLogger = mockk(relaxed = true), ) dataConnectGrpcMetadata.asClue { - it.dataConnectAuth shouldBeSameInstanceAs dataConnectAuth - it.dataConnectAppCheck shouldBeSameInstanceAs dataConnectAppCheck it.connectorLocation shouldBeSameInstanceAs connectorLocation it.kotlinVersion shouldBe "${KotlinVersion.CURRENT}" it.androidVersion shouldBe Build.VERSION.SDK_INT @@ -288,3 +373,10 @@ class DataConnectGrpcMetadataUnitTest { } } } + +private val propTestConfig = + PropTestConfig( + iterations = 100, + edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2), + shrinkingMode = ShrinkingMode.Off, + ) diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCsUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCsUnitTest.kt index 74e0d67c3e3..8a9bf2adae1 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCsUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCsUnitTest.kt @@ -30,6 +30,8 @@ import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule import com.google.firebase.dataconnect.testutil.DataConnectPath import com.google.firebase.dataconnect.testutil.newMockLogger import com.google.firebase.dataconnect.testutil.property.arbitrary.ProtoArb +import com.google.firebase.dataconnect.testutil.property.arbitrary.appCheckTokenResult +import com.google.firebase.dataconnect.testutil.property.arbitrary.authTokenResult import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnectGrpcMetadata import com.google.firebase.dataconnect.testutil.property.arbitrary.distinctPair @@ -69,6 +71,7 @@ import io.kotest.property.arbitrary.distinct import io.kotest.property.arbitrary.enum import io.kotest.property.arbitrary.next import io.kotest.property.arbitrary.of +import io.kotest.property.arbitrary.orNull import io.kotest.property.arbitrary.pair import io.kotest.property.checkAll import java.io.File @@ -112,9 +115,13 @@ class DataConnectGrpcRPCsUnitTest { fun `executeQuery(fetchPolicy=SERVER_ONLY) unconditionally returns results from server`() = runTest { val fetchPolicy1Arb = Arb.of(FetchPolicy.PREFER_CACHE, FetchPolicy.SERVER_ONLY) - checkAll(propTestConfig, QueryResultArb(entityCountRange = 0..5).pair(), fetchPolicy1Arb) { - (sample1, sample2), - fetchPolicy1 -> + checkAll( + propTestConfig, + QueryResultArb(entityCountRange = 0..5).pair(), + fetchPolicy1Arb, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.3), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.3), + ) { (sample1, sample2), fetchPolicy1, authToken, appCheckToken -> val response1 = sample1.hydratedStruct.toExecuteQueryResponse() val response2 = sample2.hydratedStruct.toExecuteQueryResponse() @@ -128,6 +135,8 @@ class DataConnectGrpcRPCsUnitTest { request, callerSdkTypeArb.bind(), fetchPolicy1, + authToken, + appCheckToken, ) server.nextResponse = response2 val result2 = @@ -136,6 +145,8 @@ class DataConnectGrpcRPCsUnitTest { request, callerSdkTypeArb.bind(), FetchPolicy.SERVER_ONLY, + authToken, + appCheckToken, ) result2.shouldBeInstanceOf().response shouldBe response2 @@ -149,7 +160,12 @@ class DataConnectGrpcRPCsUnitTest { val fetchPolicy1Arb = Arb.of(FetchPolicy.PREFER_CACHE, FetchPolicy.SERVER_ONLY) val fetchPolicy2Arb = Arb.of(FetchPolicy.PREFER_CACHE, FetchPolicy.CACHE_ONLY) val fetchPoliciesArb = Arb.pair(fetchPolicy1Arb, fetchPolicy2Arb) - checkAll(propTestConfig, fetchPoliciesArb) { (fetchPolicy1, fetchPolicy2) -> + checkAll( + propTestConfig, + fetchPoliciesArb, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.3), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.3), + ) { (fetchPolicy1, fetchPolicy2), authToken, appCheckToken -> val (sample1, sample2) = QueryResultArb(entityCountRange = 0..5, entityRepeatPolicy = INTER_SAMPLE_MUTATED) .pair() @@ -165,6 +181,8 @@ class DataConnectGrpcRPCsUnitTest { request1, callerSdkTypeArb.bind(), fetchPolicy1, + authToken, + appCheckToken, ) server.nextResponse = sample2.toExecuteQueryResponse() dataConnectGrpcRPCs.executeQuery( @@ -172,6 +190,8 @@ class DataConnectGrpcRPCsUnitTest { request2, callerSdkTypeArb.bind(), FetchPolicy.SERVER_ONLY, + authToken, + appCheckToken, ) val result = dataConnectGrpcRPCs.executeQuery( @@ -179,6 +199,8 @@ class DataConnectGrpcRPCsUnitTest { request1, callerSdkTypeArb.bind(), fetchPolicy2, + authToken, + appCheckToken, ) val expectedData = sample1.hydratedStructWithMutatedEntityValuesFrom(sample2) @@ -200,6 +222,8 @@ class DataConnectGrpcRPCsUnitTest { request, callerSdkTypeArb.next(), FetchPolicy.CACHE_ONLY, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.3).next(), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.3).next(), ) } @@ -218,9 +242,13 @@ class DataConnectGrpcRPCsUnitTest { val fetchPolicy1Arb = Arb.of(FetchPolicy.PREFER_CACHE, FetchPolicy.SERVER_ONLY) val fetchPolicy2Arb = Arb.of(FetchPolicy.entries.filterNot { it == FetchPolicy.SERVER_ONLY }) val fetchPoliciesArb = Arb.pair(fetchPolicy1Arb, fetchPolicy2Arb) - checkAll(propTestConfig, QueryResultArb(entityCountRange = 0..5), fetchPoliciesArb) { - sample, - (fetchPolicy1, fetchPolicy2) -> + checkAll( + propTestConfig, + QueryResultArb(entityCountRange = 0..5), + fetchPoliciesArb, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.3), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.3), + ) { sample, (fetchPolicy1, fetchPolicy2), authToken, appCheckToken -> startServer().use { server -> val response = sample.hydratedStruct.toExecuteQueryResponse() server.nextResponse = response @@ -233,6 +261,8 @@ class DataConnectGrpcRPCsUnitTest { request, callerSdkTypeArb.bind(), fetchPolicy1, + authToken, + appCheckToken, ) val result2 = dataConnectGrpcRPCs.executeQuery( @@ -240,6 +270,8 @@ class DataConnectGrpcRPCsUnitTest { request, callerSdkTypeArb.bind(), fetchPolicy2, + authToken, + appCheckToken, ) withClue("result1") { @@ -262,8 +294,12 @@ class DataConnectGrpcRPCsUnitTest { val fetchPolicy2Arb = Arb.of(FetchPolicy.entries.filterNot { it == FetchPolicy.SERVER_ONLY }) val fetchPoliciesArb = Arb.quadruple(fetchPolicy1Arb, fetchPolicy1Arb, fetchPolicy2Arb, fetchPolicy2Arb) - checkAll(propTestConfig, fetchPoliciesArb) { - (fetchPolicy1, fetchPolicy2, fetchPolicy3, fetchPolicy4) -> + checkAll( + propTestConfig, + fetchPoliciesArb, + Arb.dataConnect.authTokenResult().orNull(nullProbability = 0.3), + Arb.dataConnect.appCheckTokenResult().orNull(nullProbability = 0.3), + ) { (fetchPolicy1, fetchPolicy2, fetchPolicy3, fetchPolicy4), authToken, appCheckToken -> startServer().use { server -> val queryResultArb = QueryResultArb(entityCountRange = 0..5, entityRepeatPolicy = INTER_SAMPLE_MUTATED) @@ -280,6 +316,8 @@ class DataConnectGrpcRPCsUnitTest { request1, callerSdkTypeArb.bind(), fetchPolicy1, + authToken, + appCheckToken, ) server.nextResponse = sample2.toExecuteQueryResponse() dataConnectGrpcRPCs.executeQuery( @@ -287,6 +325,8 @@ class DataConnectGrpcRPCsUnitTest { request2, callerSdkTypeArb.bind(), fetchPolicy2, + authToken, + appCheckToken, ) val result1 = dataConnectGrpcRPCs.executeQuery( @@ -294,6 +334,8 @@ class DataConnectGrpcRPCsUnitTest { request1, callerSdkTypeArb.bind(), fetchPolicy3, + authToken, + appCheckToken, ) val result2 = dataConnectGrpcRPCs.executeQuery( @@ -301,6 +343,8 @@ class DataConnectGrpcRPCsUnitTest { request2, callerSdkTypeArb.bind(), fetchPolicy4, + authToken, + appCheckToken, ) withClue("result1") { diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt index 30bcd146117..af742c980fb 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt @@ -22,9 +22,7 @@ import com.google.firebase.dataconnect.DataConnectPathSegment import com.google.firebase.dataconnect.DataSource import com.google.firebase.dataconnect.FirebaseDataConnect.CallerSdkType import com.google.firebase.dataconnect.OperationRef -import com.google.firebase.dataconnect.core.DataConnectAppCheck import com.google.firebase.dataconnect.core.DataConnectAppCheck.GetAppCheckTokenResult -import com.google.firebase.dataconnect.core.DataConnectAuth import com.google.firebase.dataconnect.core.DataConnectAuth.GetAuthTokenResult import com.google.firebase.dataconnect.core.DataConnectGrpcClient import com.google.firebase.dataconnect.core.DataConnectGrpcMetadata @@ -47,14 +45,12 @@ import io.kotest.property.arbitrary.Codepoint import io.kotest.property.arbitrary.alphanumeric import io.kotest.property.arbitrary.arbitrary import io.kotest.property.arbitrary.bind -import io.kotest.property.arbitrary.constant import io.kotest.property.arbitrary.enum import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.list import io.kotest.property.arbitrary.map import io.kotest.property.arbitrary.orNull import io.kotest.property.arbitrary.string -import io.mockk.coEvery import io.mockk.mockk import kotlin.random.Random import kotlinx.serialization.DeserializationStrategy @@ -62,10 +58,6 @@ import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.modules.SerializersModule internal fun DataConnectArb.dataConnectGrpcMetadata( - dataConnectAuth: Arb = - Arb.constant(mockk(relaxed = true) { coEvery { getToken(any()) } returns null }), - dataConnectAppCheck: Arb = - Arb.constant(mockk(relaxed = true) { coEvery { getToken(any()) } returns null }), connectorLocation: Arb = connectorLocation(), kotlinVersion: Arb = Arb.string(size = 8, Codepoint.alphanumeric()), androidVersion: Arb = Arb.int(0..100), @@ -74,8 +66,6 @@ internal fun DataConnectArb.dataConnectGrpcMetadata( appId: Arb = Arb.string(size = 8, Codepoint.alphanumeric()), ): Arb = arbitrary { DataConnectGrpcMetadata( - dataConnectAuth = dataConnectAuth.bind(), - dataConnectAppCheck = dataConnectAppCheck.bind(), connectorLocation = connectorLocation.bind(), kotlinVersion = kotlinVersion.bind(), androidVersion = androidVersion.bind(), @@ -340,13 +330,12 @@ internal inline fun DataConnectArb.operationRefConstru } internal fun DataConnectArb.authTokenResult( - accessToken: Arb = accessToken().orNull(nullProbability = 0.33), - authUid: Arb = - Arb.string(0..10, Codepoint.alphanumeric()).orNull(nullProbability = 0.33), + accessToken: Arb = authToken().orNull(nullProbability = 0.33), + authUid: Arb = authUid().orNull(nullProbability = 0.33), ): Arb = Arb.bind(accessToken, authUid, ::GetAuthTokenResult) internal fun DataConnectArb.appCheckTokenResult( - accessToken: Arb = accessToken().orNull(nullProbability = 0.33), + accessToken: Arb = appCheckToken().orNull(nullProbability = 0.33), ): Arb = accessToken.map { GetAppCheckTokenResult(it) } internal fun DataConnectArb.semanticVersion( diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt index fd1fe1f1505..99ed775372f 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt @@ -145,9 +145,15 @@ object DataConnectArb { } } - fun accessToken( + fun authUid(string: Arb = Arb.string(size = 8, Codepoint.alphanumeric())): Arb = + string.map { "authUid_${it.lowercase()}" } + + fun authToken(string: Arb = Arb.string(size = 8, Codepoint.alphanumeric())): Arb = + string.map { "authToken_${it.lowercase()}" } + + fun appCheckToken( string: Arb = Arb.string(size = 8, Codepoint.alphanumeric()) - ): Arb = arbitrary { "accessToken_${string.bind()}" } + ): Arb = string.map { "appCheckToken_${it.lowercase()}" } fun requestId(string: Arb = Arb.string(size = 8, Codepoint.alphanumeric())): Arb = arbitrary { diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt index 8864cb7442d..b6cfff84376 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt @@ -22,11 +22,22 @@ import io.kotest.assertions.print.print import io.kotest.property.Arb import io.kotest.property.arbitrary.bind -data class TwoValues(val value1: T, val value2: T) { +interface MultipleValues : Iterable { + + fun toList(): List + + override fun iterator(): Iterator = toList().iterator() +} + +fun MultipleValues.iterator(): Iterator = toList().iterator() + +fun > MultipleValues.sorted(): List = toList().sorted() + +data class TwoValues(val value1: T, val value2: T) : MultipleValues { override fun toString() = "TwoValues(value1=${value1.print().value}, value2=${value2.print().value})" - fun toList(): List = listOf(value1, value2) + override fun toList(): List = listOf(value1, value2) } fun List.toTwoValues(): TwoValues { @@ -37,14 +48,14 @@ fun List.toTwoValues(): TwoValues { fun > TwoValues.sorted(): TwoValues = if (value1 <= value2) this else TwoValues(value2, value1) -data class ThreeValues(val value1: T, val value2: T, val value3: T) { +data class ThreeValues(val value1: T, val value2: T, val value3: T) : MultipleValues { override fun toString() = "ThreeValues(" + "value1=${value1.print().value}, " + "value2=${value2.print().value}, " + "value3=${value3.print().value})" - fun toList(): List = listOf(value1, value2, value3) + override fun toList(): List = listOf(value1, value2, value3) } fun > ThreeValues.sorted(): ThreeValues { @@ -61,7 +72,8 @@ fun List.toThreeValues(): ThreeValues { return ThreeValues(get(0), get(1), get(2)) } -data class FourValues(val value1: T, val value2: T, val value3: T, val value4: T) { +data class FourValues(val value1: T, val value2: T, val value3: T, val value4: T) : + MultipleValues { override fun toString() = "FourValues(" + "value1=${value1.print().value}, " + @@ -69,7 +81,7 @@ data class FourValues(val value1: T, val value2: T, val value3: T, val value4 "value3=${value3.print().value}, " + "value4=${value4.print().value})" - fun toList(): List = listOf(value1, value2, value3, value4) + override fun toList(): List = listOf(value1, value2, value3, value4) } fun List.toFourValues(): FourValues { @@ -92,7 +104,7 @@ data class FiveValues( val value3: T, val value4: T, val value5: T -) { +) : MultipleValues { override fun toString() = "FiveValues(" + "value1=${value1.print().value}, " + @@ -101,7 +113,7 @@ data class FiveValues( "value4=${value4.print().value}, " + "value5=${value5.print().value})" - fun toList(): List = listOf(value1, value2, value3, value4, value5) + override fun toList(): List = listOf(value1, value2, value3, value4, value5) } fun List.toFiveValues(): FiveValues { From fb205679791cfb6dce57895488747333ad99b293 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Apr 2026 16:57:52 -0400 Subject: [PATCH 2/4] CHANGELOG.md: update PR number --- firebase-dataconnect/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index 6639a3433e4..20902693b9e 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -10,7 +10,7 @@ - [changed] Internal refactor for calculating debug logging strings. ([#8024](https://github.com/firebase/firebase-android-sdk/pull/8024)) - [changed] Internal refactor to use token objects instead of strings. - ([#NNNN](https://github.com/firebase/firebase-android-sdk/pull/NNNN)) + ([#8027](https://github.com/firebase/firebase-android-sdk/pull/8027)) # 17.2.0 From 3f7176a3a7a4c68ed2e82a997d19e4f8984059fb Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 8 Apr 2026 17:20:03 -0400 Subject: [PATCH 3/4] DataConnectGrpcMetadata.kt: remove unused method: StructProtoBuilder.putHeaders() --- .../core/DataConnectGrpcMetadata.kt | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt index a0569260028..33a69b95613 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt @@ -24,7 +24,6 @@ import com.google.firebase.dataconnect.core.Globals.toScrubbedAccessToken import com.google.firebase.dataconnect.core.LoggerGlobals.Logger import com.google.firebase.dataconnect.core.LoggerGlobals.debug import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto -import com.google.firebase.dataconnect.util.StructProtoBuilder import com.google.protobuf.Struct import io.grpc.Metadata @@ -99,7 +98,6 @@ internal class DataConnectGrpcMetadata( companion object { // TODO: Move this to ProtoUtil.kt where it would live alongside other related methods. - // NOTE: Keep the implementation of this method in parity with StructProtoBuilder.putHeaders(). fun Metadata.toStructProto(authUid: String?): Struct = buildStructProto { val keys: List> = run { val keySet: MutableSet = keys().toMutableSet() @@ -129,36 +127,6 @@ internal class DataConnectGrpcMetadata( } } - // TODO: Move this to ProtoUtil.kt where it would live alongside other related methods. - // NOTE: Keep the implementation of this method in parity with Metadata.toStructProto(). - fun StructProtoBuilder.putHeaders(key: String, headers: Map) { - putStruct(key) { - val keys: List = - buildSet { - addAll(headers.keys) - // Always explicitly include the auth header in the returned string, even if it is - // absent. - add(firebaseAuthTokenHeader.name()) - add(firebaseAppCheckTokenHeader.name()) - } - .sorted() - - keys.forEach { key -> - val value = headers[key] - val scrubbedValue = - value?.let { - when (key) { - firebaseAuthTokenHeader.name() -> it.toScrubbedAccessToken() - firebaseAppCheckTokenHeader.name() -> it.toScrubbedAccessToken() - else -> it - } - } - - put(key, scrubbedValue) - } - } - } - private val firebaseAuthTokenHeader: Metadata.Key = Metadata.Key.of("x-firebase-auth-token", Metadata.ASCII_STRING_MARSHALLER) From 1a44ea9258683c2748641d71e9d7c2b4d129b271 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Thu, 9 Apr 2026 00:57:17 -0400 Subject: [PATCH 4/4] tuples.kt: remove redundant method: `fun MultipleValues.iterator(): Iterator` --- .../firebase/dataconnect/testutil/property/arbitrary/tuples.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt index b6cfff84376..c311cde1021 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/tuples.kt @@ -29,8 +29,6 @@ interface MultipleValues : Iterable { override fun iterator(): Iterator = toList().iterator() } -fun MultipleValues.iterator(): Iterator = toList().iterator() - fun > MultipleValues.sorted(): List = toList().sorted() data class TwoValues(val value1: T, val value2: T) : MultipleValues {