diff --git a/firebase-dataconnect/api.txt b/firebase-dataconnect/api.txt index 6d27b5db283..49f25a6ef19 100644 --- a/firebase-dataconnect/api.txt +++ b/firebase-dataconnect/api.txt @@ -193,6 +193,9 @@ package com.google.firebase.dataconnect.core { public final class LoggerKt { } + public final class MutationRefImplKt { + } + public final class QueryRefImplKt { } diff --git a/firebase-dataconnect/firebase-dataconnect.gradle.kts b/firebase-dataconnect/firebase-dataconnect.gradle.kts index 35ff9a0ee7c..869fbc4a277 100644 --- a/firebase-dataconnect/firebase-dataconnect.gradle.kts +++ b/firebase-dataconnect/firebase-dataconnect.gradle.kts @@ -148,6 +148,7 @@ dependencies { testImplementation(libs.androidx.test.junit) testImplementation(libs.kotest.assertions) testImplementation(libs.kotest.property) + testImplementation(libs.kotest.property.arbs) testImplementation(libs.mockk) testImplementation(libs.robolectric) testImplementation(libs.truth) @@ -173,6 +174,7 @@ dependencies { androidTestImplementation(libs.kotlin.coroutines.test) androidTestImplementation(libs.kotest.assertions) androidTestImplementation(libs.kotest.property) + androidTestImplementation(libs.kotest.property.arbs) androidTestImplementation(libs.mockk) androidTestImplementation(libs.mockk.android) androidTestImplementation(libs.truth) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/MutationRefImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/MutationRefImpl.kt index ed679e59425..8fcccfc3c09 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/MutationRefImpl.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/MutationRefImpl.kt @@ -94,3 +94,45 @@ internal class MutationRefImpl( override fun toString() = "MutationResultImpl(data=$data, ref=$ref)" } } + +internal fun MutationRefImpl.copy( + dataConnect: FirebaseDataConnectInternal = this.dataConnect, + operationName: String = this.operationName, + variables: Variables = this.variables, + dataDeserializer: DeserializationStrategy = this.dataDeserializer, + variablesSerializer: SerializationStrategy = this.variablesSerializer, + isFromGeneratedSdk: Boolean = this.isFromGeneratedSdk, +) = + MutationRefImpl( + dataConnect = dataConnect, + operationName = operationName, + variables = variables, + dataDeserializer = dataDeserializer, + variablesSerializer = variablesSerializer, + isFromGeneratedSdk = isFromGeneratedSdk, + ) + +internal fun MutationRefImpl.withVariablesSerializer( + variables: NewVariables, + variablesSerializer: SerializationStrategy, +): MutationRefImpl = + MutationRefImpl( + dataConnect = dataConnect, + operationName = operationName, + variables = variables, + dataDeserializer = dataDeserializer, + variablesSerializer = variablesSerializer, + isFromGeneratedSdk = isFromGeneratedSdk, + ) + +internal fun MutationRefImpl<*, Variables>.withDataDeserializer( + dataDeserializer: DeserializationStrategy, +): MutationRefImpl = + MutationRefImpl( + dataConnect = dataConnect, + operationName = operationName, + variables = variables, + dataDeserializer = dataDeserializer, + variablesSerializer = variablesSerializer, + isFromGeneratedSdk = isFromGeneratedSdk, + ) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QueryRefImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QueryRefImpl.kt index 90e345531f4..ddffba685c5 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QueryRefImpl.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QueryRefImpl.kt @@ -69,12 +69,19 @@ internal class QueryRefImpl( } } -internal fun QueryRefImpl.withVariables(variables: Variables) = +internal fun QueryRefImpl.copy( + dataConnect: FirebaseDataConnectInternal = this.dataConnect, + operationName: String = this.operationName, + variables: Variables = this.variables, + dataDeserializer: DeserializationStrategy = this.dataDeserializer, + variablesSerializer: SerializationStrategy = this.variablesSerializer, + isFromGeneratedSdk: Boolean = this.isFromGeneratedSdk, +) = QueryRefImpl( - dataConnect, - operationName, - variables, - dataDeserializer, - variablesSerializer, - isFromGeneratedSdk, + dataConnect = dataConnect, + operationName = operationName, + variables = variables, + dataDeserializer = dataDeserializer, + variablesSerializer = variablesSerializer, + isFromGeneratedSdk = isFromGeneratedSdk, ) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QuerySubscriptionImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QuerySubscriptionImpl.kt index c6e8049e314..ca8496be8ec 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QuerySubscriptionImpl.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QuerySubscriptionImpl.kt @@ -69,7 +69,7 @@ internal class QuerySubscriptionImpl(query: QueryRefImpl + val result = operationResult.deserialize(DataConnectUntypedData) + + result.asClue { + if (operationResult.data === null) { + it.data.shouldBeNull() + } else { + it.data shouldBe operationResult.data.toMap() + } + it.errors shouldContainExactly operationResult.errors + } + } + } + + @Test + fun `deserialize() should throw if one or more errors and data is null`() = runTest { + val arb = Arb.operationResult().filter { it.errors.isNotEmpty() }.map { it.copy(data = null) } + checkAll(iterations = 5, arb) { operationResult -> + val exception = + shouldThrow { operationResult.deserialize(mockk()) } + exception.message shouldContain "${operationResult.errors}" + } + } + + @Test + fun `deserialize() should throw if one or more errors and data is _not_ null`() = runTest { + val arb = Arb.operationResult().filter { it.data !== null && it.errors.isNotEmpty() } + checkAll(iterations = 5, arb) { operationResult -> + val exception = + shouldThrow { operationResult.deserialize(mockk()) } + exception.message shouldContain "${operationResult.errors}" + } + } + + @Test + fun `deserialize() should throw if data is null and errors is empty`() { + val operationResult = OperationResult(data = null, errors = emptyList()) + val exception = + shouldThrow { operationResult.deserialize(mockk()) } + exception.message shouldContain "no data" + } + + @Test + fun `deserialize() successfully deserializes`() = runTest { + val testData = TestData(Arb.firstName().next().name) + val operationResult = OperationResult(encodeToStruct(testData), errors = emptyList()) + + val deserializedData = operationResult.deserialize(serializer()) + + deserializedData shouldBe testData + } + + @Test + fun `deserialize() throws if decoding fails`() = runTest { + val data = buildStructProto { put("zzzz", 42) } + val operationResult = OperationResult(data, errors = emptyList()) + shouldThrow { operationResult.deserialize(serializer()) } + } + + @Test + fun `deserialize() re-throws DataConnectException`() = runTest { + val data = encodeToStruct(TestData("fe45zhyd3m")) + val operationResult = OperationResult(data = data, errors = emptyList()) + val deserializer: DeserializationStrategy = spyk(serializer()) + val exception = DataConnectException(message = Arb.airline().next().name) + every { deserializer.deserialize(any()) } throws (exception) + + val thrownException = + shouldThrow { operationResult.deserialize(deserializer) } + + thrownException shouldBeSameInstanceAs exception + } + + @Test + fun `deserialize() wraps non-DataConnectException in DataConnectException`() = runTest { + val data = encodeToStruct(TestData("rbmkny6b4r")) + val operationResult = OperationResult(data = data, errors = emptyList()) + val deserializer: DeserializationStrategy = spyk(serializer()) + class MyException : Exception("y3cx44q43q") + val exception = MyException() + every { deserializer.deserialize(any()) } throws (exception) + + val thrownException = + shouldThrow { operationResult.deserialize(deserializer) } + + thrownException.cause shouldBeSameInstanceAs exception + } + + @Serializable data class TestData(val foo: String) +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt new file mode 100644 index 00000000000..0269a1ff403 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/MutationRefImplUnitTest.kt @@ -0,0 +1,419 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect.core + +import com.google.firebase.dataconnect.DataConnectException +import com.google.firebase.dataconnect.DataConnectUntypedData +import com.google.firebase.dataconnect.DataConnectUntypedVariables +import com.google.firebase.dataconnect.core.DataConnectGrpcClient.OperationResult +import com.google.firebase.dataconnect.testutil.dataConnectError +import com.google.firebase.dataconnect.testutil.mutationRefImpl +import com.google.firebase.dataconnect.util.SuspendingLazy +import com.google.firebase.dataconnect.util.buildStructProto +import com.google.firebase.dataconnect.util.encodeToStruct +import com.google.firebase.dataconnect.util.toStructProto +import com.google.protobuf.Struct +import io.kotest.assertions.asClue +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.retry +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotBeBlank +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.matchers.types.shouldNotBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.arbitrary.Codepoint +import io.kotest.property.arbitrary.alphanumeric +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.string +import io.mockk.CapturingSlot +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import kotlin.time.Duration +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import org.junit.Test + +@Suppress("ReplaceCallWithBinaryOperator") +@OptIn(ExperimentalCoroutinesApi::class) +class MutationRefImplUnitTest { + + @Serializable private data class TestData(val foo: String) + @Serializable private data class TestVariables(val bar: String) + + @Test + fun `execute() returns the result on success`() = runTest { + val data = Arb.testData().next() + val operationResult = OperationResult(encodeToStruct(data), errors = emptyList()) + val dataConnect = dataConnectWithMutationResult(Result.success(operationResult)) + val mutationRefImpl = Arb.mutationRefImpl().next().copy(dataConnect = dataConnect) + + val mutationResult = mutationRefImpl.execute() + + assertSoftly { + mutationResult.ref shouldBeSameInstanceAs mutationRefImpl + mutationResult.data shouldBe data + } + } + + @Test + fun `execute() calls executeMutation with the correct arguments`() = runTest { + val data = Arb.testData().next() + val operationResult = OperationResult(encodeToStruct(data), errors = emptyList()) + val requestIdSlot: CapturingSlot = slot() + val operationNameSlot: CapturingSlot = slot() + val variablesSlot: CapturingSlot = slot() + val dataConnect = + dataConnectWithMutationResult( + Result.success(operationResult), + requestIdSlot, + operationNameSlot, + variablesSlot + ) + val mutationRefImpl = Arb.mutationRefImpl().next().copy(dataConnect = dataConnect) + + mutationRefImpl.execute() + val requestId1 = requestIdSlot.captured + val operationName1 = operationNameSlot.captured + val variables1 = variablesSlot.captured + + requestIdSlot.clear() + operationNameSlot.clear() + variablesSlot.clear() + mutationRefImpl.execute() + val requestId2 = requestIdSlot.captured + val operationName2 = operationNameSlot.captured + val variables2 = variablesSlot.captured + + assertSoftly { + requestId1.shouldNotBeBlank() + requestId2.shouldNotBeBlank() + requestId1 shouldNotBe requestId2 + operationName1 shouldBe mutationRefImpl.operationName + operationName2 shouldBe operationName1 + variables1 shouldBe encodeToStruct(mutationRefImpl.variables) + variables2 shouldBe variables1 + } + } + + @Test + fun `execute() calls executeMutation with the correct isFromGeneratedSdk`() { + assertSoftly { + for (isFromGeneratedSdk in listOf(true, false)) { + withClue("isFromGeneratedSdk=$isFromGeneratedSdk") { + verifyIsFromGeneratedSdkRoundTrip(isFromGeneratedSdk) + } + } + } + } + + private fun verifyIsFromGeneratedSdkRoundTrip(isFromGeneratedSdk: Boolean) = runTest { + val data = Arb.testData().next() + val operationResult = OperationResult(encodeToStruct(data), errors = emptyList()) + val isFromGeneratedSdkSlot: CapturingSlot = slot() + val dataConnect = + dataConnectWithMutationResult( + Result.success(operationResult), + isFromGeneratedSdkSlot = isFromGeneratedSdkSlot + ) + val mutationRefImpl = + Arb.mutationRefImpl() + .next() + .copy(dataConnect = dataConnect, isFromGeneratedSdk = isFromGeneratedSdk) + + mutationRefImpl.execute() + val isFromGeneratedSdk1 = isFromGeneratedSdkSlot.captured + isFromGeneratedSdkSlot.clear() + mutationRefImpl.execute() + val isFromGeneratedSdk2 = isFromGeneratedSdkSlot.captured + + assertSoftly { + isFromGeneratedSdk1 shouldBe isFromGeneratedSdk + isFromGeneratedSdk2 shouldBe isFromGeneratedSdk + } + } + + @Test + fun `execute() handles DataConnectUntypedVariables and DataConnectUntypedData`() = runTest { + val variables = DataConnectUntypedVariables("foo" to 42.0) + val errors = listOf(Arb.dataConnectError().next()) + val data = DataConnectUntypedData(mapOf("bar" to 24.0), errors) + val variablesSlot: CapturingSlot = slot() + val operationResult = OperationResult(buildStructProto { put("bar", 24.0) }, errors) + val dataConnect = + dataConnectWithMutationResult(Result.success(operationResult), variablesSlot = variablesSlot) + val mutationRefImpl = + Arb.mutationRefImpl() + .next() + .copy(dataConnect = dataConnect) + .withVariablesSerializer(variables, DataConnectUntypedVariables) + .withDataDeserializer(DataConnectUntypedData) + + val mutationResult = mutationRefImpl.execute() + + assertSoftly { + mutationResult.ref shouldBeSameInstanceAs mutationRefImpl + mutationResult.data shouldBe data + variablesSlot.captured shouldBe variables.variables.toStructProto() + } + } + + @Test + fun `execute() throws when the data is null`() = runTest { + val operationResult = OperationResult(data = null, errors = emptyList()) + val dataConnect = dataConnectWithMutationResult(Result.success(operationResult)) + val mutationRefImpl = Arb.mutationRefImpl().next().copy(dataConnect = dataConnect) + + shouldThrow { mutationRefImpl.execute() } + } + + @Test + fun `constructor assigns public properties to the given arguments`() { + val values = Arb.mutationRefImpl().next() + val mutationRefImpl = + MutationRefImpl( + dataConnect = values.dataConnect, + operationName = values.operationName, + variables = values.variables, + dataDeserializer = values.dataDeserializer, + variablesSerializer = values.variablesSerializer, + isFromGeneratedSdk = values.isFromGeneratedSdk, + ) + + mutationRefImpl.asClue { + assertSoftly { + it.dataConnect shouldBeSameInstanceAs values.dataConnect + it.operationName shouldBeSameInstanceAs values.operationName + it.variables shouldBeSameInstanceAs values.variables + it.dataDeserializer shouldBeSameInstanceAs values.dataDeserializer + it.variablesSerializer shouldBeSameInstanceAs values.variablesSerializer + it.isFromGeneratedSdk shouldBe values.isFromGeneratedSdk + } + } + } + + @Test + fun `hashCode() should return the same value when invoked repeatedly`() { + val mutationRefImpl: MutationRefImpl<*, *> = Arb.mutationRefImpl().next() + val hashCode = mutationRefImpl.hashCode() + repeat(10) { mutationRefImpl.hashCode() shouldBe hashCode } + } + + @Test + fun `hashCode() should return the same value when invoked on distinct, but equal, objects`() { + val mutationRefImpl1: MutationRefImpl<*, *> = Arb.mutationRefImpl().next() + val mutationRefImpl2: MutationRefImpl<*, *> = mutationRefImpl1.copy() + mutationRefImpl1 shouldNotBeSameInstanceAs mutationRefImpl2 // verify test precondition + repeat(10) { mutationRefImpl1.hashCode() shouldBe mutationRefImpl2.hashCode() } + } + + @Test + fun `hashCode() should incorporate dataConnect`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(dataConnect = mockk(name = stringArb.next())) } + } + + @Test + fun `hashCode() should incorporate operationName`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(operationName = stringArb.next()) } + } + + @Test + fun `hashCode() should incorporate variables`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(variables = TestVariables(stringArb.next())) } + } + + @Test + fun `hashCode() should incorporate dataDeserializer`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(dataDeserializer = mockk(name = stringArb.next())) } + } + + @Test + fun `hashCode() should incorporate variablesSerializer`() = runTest { + verifyHashCodeEventuallyDiffers { + it.copy(variablesSerializer = mockk(name = stringArb.next())) + } + } + + @Test + fun `hashCode() should NOT incorporate isFromGeneratedSdk`() = runTest { + val mutationRef1 = Arb.mutationRefImpl().next() + val mutationRef2 = mutationRef1.copy(isFromGeneratedSdk = !mutationRef1.isFromGeneratedSdk) + mutationRef1.hashCode() shouldBe mutationRef2.hashCode() + } + + private suspend fun verifyHashCodeEventuallyDiffers( + otherFactory: + (other: MutationRefImpl) -> MutationRefImpl + ) { + val obj1: MutationRefImpl = Arb.mutationRefImpl().next() + retry(maxRetry = 50, timeout = Duration.INFINITE) { + val obj2: MutationRefImpl = otherFactory(obj1) + obj1.hashCode() shouldNotBe obj2.hashCode() + } + } + + @Test + fun `equals(this) should return true`() = runTest { + val mutationRefImpl: MutationRefImpl = Arb.mutationRefImpl().next() + mutationRefImpl.equals(mutationRefImpl) shouldBe true + } + + @Test + fun `equals(equal, but distinct, instance) should return true`() = runTest { + val mutationRefImpl1: MutationRefImpl = Arb.mutationRefImpl().next() + val mutationRefImpl2: MutationRefImpl = mutationRefImpl1.copy() + mutationRefImpl1 shouldNotBeSameInstanceAs mutationRefImpl2 // verify test precondition + mutationRefImpl1.equals(mutationRefImpl2) shouldBe true + } + + @Test + fun `equals(null) should return false`() = runTest { + val mutationRefImpl: MutationRefImpl = Arb.mutationRefImpl().next() + mutationRefImpl.equals(null) shouldBe false + } + + @Test + fun `equals(an object of a different type) should return false`() = runTest { + val mutationRefImpl: MutationRefImpl = Arb.mutationRefImpl().next() + mutationRefImpl.equals("not a MutationRefImpl") shouldBe false + } + + @Test + fun `equals() should return false when only dataConnect differs`() = runTest { + val mutationRefImpl1: MutationRefImpl = Arb.mutationRefImpl().next() + val mutationRefImpl2 = mutationRefImpl1.copy(dataConnect = mockk(stringArb.next())) + mutationRefImpl1.equals(mutationRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only operationName differs`() = runTest { + val mutationRefImpl1: MutationRefImpl = Arb.mutationRefImpl().next() + val mutationRefImpl2 = + mutationRefImpl1.copy(operationName = mutationRefImpl1.operationName + "2") + mutationRefImpl1.equals(mutationRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only variables differs`() = runTest { + val mutationRefImpl1: MutationRefImpl = Arb.mutationRefImpl().next() + val mutationRefImpl2 = + mutationRefImpl1.copy(variables = TestVariables(mutationRefImpl1.variables.bar + "2")) + mutationRefImpl1.equals(mutationRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only dataDeserializer differs`() = runTest { + val mutationRefImpl1: MutationRefImpl = Arb.mutationRefImpl().next() + val mutationRefImpl2 = mutationRefImpl1.copy(dataDeserializer = mockk(stringArb.next())) + mutationRefImpl1.equals(mutationRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only variablesSerializer differs`() = runTest { + val mutationRefImpl1: MutationRefImpl = Arb.mutationRefImpl().next() + val mutationRefImpl2 = mutationRefImpl1.copy(variablesSerializer = mockk(stringArb.next())) + mutationRefImpl1.equals(mutationRefImpl2) shouldBe false + } + + @Test + fun `equals() should return _TRUE_ when only isFromGeneratedSdk differs`() = runTest { + val mutationRefImpl1: MutationRefImpl = Arb.mutationRefImpl().next() + val mutationRefImpl2 = + mutationRefImpl1.copy(isFromGeneratedSdk = !mutationRefImpl1.isFromGeneratedSdk) + mutationRefImpl1.equals(mutationRefImpl2) shouldBe true + } + + @Test + fun `toString() should incorporate the string representations of public properties`() = runTest { + val mutationRefImpl: MutationRefImpl = Arb.mutationRefImpl().next() + val mutationRefImpls = + listOf( + mutationRefImpl, + mutationRefImpl.copy(isFromGeneratedSdk = !mutationRefImpl.isFromGeneratedSdk), + ) + val toStringResult = mutationRefImpl.toString() + + assertSoftly { + mutationRefImpls.forEach { + it.asClue { + toStringResult.shouldContain("dataConnect=${mutationRefImpl.dataConnect}") + toStringResult.shouldContain("operationName=${mutationRefImpl.operationName}") + toStringResult.shouldContain("variables=${mutationRefImpl.variables}") + toStringResult.shouldContain("dataDeserializer=${mutationRefImpl.dataDeserializer}") + toStringResult.shouldContain("variablesSerializer=${mutationRefImpl.variablesSerializer}") + } + } + } + } + + private companion object { + val stringArb = Arb.string(6, codepoints = Codepoint.alphanumeric()) + + fun Arb.Companion.testVariables(): Arb = arbitrary { + val stringArb = Arb.string(6, Codepoint.alphanumeric()) + TestVariables(stringArb.bind()) + } + + fun Arb.Companion.testData(): Arb = arbitrary { + val stringArb = Arb.string(6, Codepoint.alphanumeric()) + TestData(stringArb.bind()) + } + + fun Arb.Companion.mutationRefImpl(): Arb> = + mutationRefImpl(Arb.testVariables()).map { + it.copy( + variablesSerializer = serializer(), + dataDeserializer = serializer() + ) + } + + fun TestScope.dataConnectWithMutationResult( + result: Result, + requestIdSlot: CapturingSlot = slot(), + operationNameSlot: CapturingSlot = slot(), + variablesSlot: CapturingSlot = slot(), + isFromGeneratedSdkSlot: CapturingSlot = slot(), + ): FirebaseDataConnectInternal = + mockk(relaxed = true) { + every { blockingDispatcher } returns UnconfinedTestDispatcher(testScheduler) + every { lazyGrpcClient } returns + SuspendingLazy { + mockk { + coEvery { + executeMutation( + capture(requestIdSlot), + capture(operationNameSlot), + capture(variablesSlot), + capture(isFromGeneratedSdkSlot), + ) + } returns result.getOrThrow() + } + } + } + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/OperationRefImplUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/OperationRefImplUnitTest.kt new file mode 100644 index 00000000000..fbc8b9703dd --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/OperationRefImplUnitTest.kt @@ -0,0 +1,216 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect.core + +import com.google.firebase.dataconnect.testutil.StubOperationRefImpl +import com.google.firebase.dataconnect.testutil.copy +import com.google.firebase.dataconnect.testutil.operationRefImpl +import io.kotest.assertions.asClue +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.retry +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.matchers.types.shouldNotBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.arbitrary.Codepoint +import io.kotest.property.arbitrary.alphanumeric +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.string +import io.mockk.mockk +import kotlin.time.Duration +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@Suppress("ReplaceCallWithBinaryOperator") +class OperationRefImplUnitTest { + + private class TestData + private class TestVariables(val bar: String) + + @Test + fun `constructor assigns public properties to the given arguments`() { + val values = Arb.operationRefImpl().next() + val operationRefImpl = + object : + OperationRefImpl( + dataConnect = values.dataConnect, + operationName = values.operationName, + variables = values.variables, + dataDeserializer = values.dataDeserializer, + variablesSerializer = values.variablesSerializer, + ) { + override suspend fun execute() = TODO() + } + + operationRefImpl.asClue { + assertSoftly { + it.dataConnect shouldBeSameInstanceAs values.dataConnect + it.operationName shouldBeSameInstanceAs values.operationName + it.variables shouldBeSameInstanceAs values.variables + it.dataDeserializer shouldBeSameInstanceAs values.dataDeserializer + it.variablesSerializer shouldBeSameInstanceAs values.variablesSerializer + } + } + } + + @Test + fun `hashCode() should return the same value when invoked repeatedly`() { + val operationRefImpl: OperationRefImpl<*, *> = Arb.operationRefImpl().next() + val hashCode = operationRefImpl.hashCode() + repeat(10) { operationRefImpl.hashCode() shouldBe hashCode } + } + + @Test + fun `hashCode() should return the same value when invoked on distinct, but equal, objects`() { + val operationRefImpl1 = Arb.operationRefImpl().next() + val operationRefImpl2 = operationRefImpl1.copy() + operationRefImpl1 shouldNotBeSameInstanceAs operationRefImpl2 // verify test precondition + repeat(10) { operationRefImpl1.hashCode() shouldBe operationRefImpl2.hashCode() } + } + + @Test + fun `hashCode() should incorporate dataConnect`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(dataConnect = mockk(name = stringArb.next())) } + } + + @Test + fun `hashCode() should incorporate operationName`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(operationName = stringArb.next()) } + } + + @Test + fun `hashCode() should incorporate variables`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(variables = TestVariables(stringArb.next())) } + } + + @Test + fun `hashCode() should incorporate dataDeserializer`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(dataDeserializer = mockk(name = stringArb.next())) } + } + + @Test + fun `hashCode() should incorporate variablesSerializer`() = runTest { + verifyHashCodeEventuallyDiffers { + it.copy(variablesSerializer = mockk(name = stringArb.next())) + } + } + + private suspend fun verifyHashCodeEventuallyDiffers( + otherFactory: + (other: StubOperationRefImpl) -> StubOperationRefImpl< + TestData, TestVariables + > + ) { + val obj1: StubOperationRefImpl = Arb.operationRefImpl().next() + retry(maxRetry = 50, timeout = Duration.INFINITE) { + val obj2 = otherFactory(obj1) + obj1.hashCode() shouldNotBe obj2.hashCode() + } + } + + @Test + fun `equals(this) should return true`() = runTest { + val operationRefImpl = Arb.operationRefImpl().next() + operationRefImpl.equals(operationRefImpl) shouldBe true + } + + @Test + fun `equals(equal, but distinct, instance) should return true`() = runTest { + val operationRefImpl1 = Arb.operationRefImpl().next() + val operationRefImpl2 = operationRefImpl1.copy() + operationRefImpl1 shouldNotBeSameInstanceAs operationRefImpl2 // verify test precondition + operationRefImpl1.equals(operationRefImpl2) shouldBe true + } + + @Test + fun `equals(null) should return false`() = runTest { + val operationRefImpl = Arb.operationRefImpl().next() + operationRefImpl.equals(null) shouldBe false + } + + @Test + fun `equals(an object of a different type) should return false`() = runTest { + val operationRefImpl = Arb.operationRefImpl().next() + operationRefImpl.equals("not an OperationRefImpl") shouldBe false + } + + @Test + fun `equals() should return false when only dataConnect differs`() = runTest { + val operationRefImpl1 = Arb.operationRefImpl().next() + val operationRefImpl2 = operationRefImpl1.copy(dataConnect = mockk(stringArb.next())) + operationRefImpl1.equals(operationRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only operationName differs`() = runTest { + val operationRefImpl1 = Arb.operationRefImpl().next() + val operationRefImpl2 = + operationRefImpl1.copy(operationName = operationRefImpl1.operationName + "2") + operationRefImpl1.equals(operationRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only variables differs`() = runTest { + val operationRefImpl1 = Arb.operationRefImpl().next() + val operationRefImpl2 = + operationRefImpl1.copy(variables = TestVariables(operationRefImpl1.variables.bar + "2")) + operationRefImpl1.equals(operationRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only dataDeserializer differs`() = runTest { + val operationRefImpl1 = Arb.operationRefImpl().next() + val operationRefImpl2 = operationRefImpl1.copy(dataDeserializer = mockk(stringArb.next())) + operationRefImpl1.equals(operationRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only variablesSerializer differs`() = runTest { + val operationRefImpl1 = Arb.operationRefImpl().next() + val operationRefImpl2 = operationRefImpl1.copy(variablesSerializer = mockk(stringArb.next())) + operationRefImpl1.equals(operationRefImpl2) shouldBe false + } + + @Test + fun `toString() should incorporate the string representations of public properties`() = runTest { + val operationRefImpl = Arb.operationRefImpl().next() + val toStringResult = operationRefImpl.toString() + + assertSoftly { + toStringResult.shouldContain("dataConnect=${operationRefImpl.dataConnect}") + toStringResult.shouldContain("operationName=${operationRefImpl.operationName}") + toStringResult.shouldContain("variables=${operationRefImpl.variables}") + toStringResult.shouldContain("dataDeserializer=${operationRefImpl.dataDeserializer}") + toStringResult.shouldContain("variablesSerializer=${operationRefImpl.variablesSerializer}") + } + } + + private companion object { + val stringArb = Arb.string(6, codepoints = Codepoint.alphanumeric()) + + fun Arb.Companion.testVariables(): Arb = arbitrary { + val stringArb = Arb.string(6, Codepoint.alphanumeric()) + TestVariables(stringArb.bind()) + } + + fun Arb.Companion.operationRefImpl(): Arb> = + operationRefImpl(Arb.testVariables()) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/QueryRefImplUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/QueryRefImplUnitTest.kt new file mode 100644 index 00000000000..5c5f7a3749d --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/QueryRefImplUnitTest.kt @@ -0,0 +1,304 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect.core + +import com.google.firebase.dataconnect.querymgr.QueryManager +import com.google.firebase.dataconnect.testutil.queryRefImpl +import com.google.firebase.dataconnect.util.SequencedReference +import com.google.firebase.dataconnect.util.SuspendingLazy +import io.kotest.assertions.asClue +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.retry +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.kotest.matchers.types.shouldNotBeSameInstanceAs +import io.kotest.property.Arb +import io.kotest.property.arbitrary.Codepoint +import io.kotest.property.arbitrary.alphanumeric +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.next +import io.kotest.property.arbitrary.string +import io.mockk.CapturingSlot +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import kotlin.time.Duration +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@Suppress("ReplaceCallWithBinaryOperator") +class QueryRefImplUnitTest { + + private class TestData(val foo: String) + private class TestVariables(val bar: String) + + @Test + fun `execute() returns the result on success`() = runTest { + val data = TestData("gy54w6f5be") + val querySlot = slot>() + val dataConnect = dataConnectWithQueryResult(Result.success(data), querySlot) + val queryRefImpl = Arb.queryRefImpl().next().copy(dataConnect = dataConnect) + + val queryResult = queryRefImpl.execute() + + assertSoftly { + queryResult.ref shouldBeSameInstanceAs queryRefImpl + queryResult.data shouldBe data + querySlot.captured shouldBeSameInstanceAs queryRefImpl + } + } + + @Test + fun `execute() throws on failure`() = runTest { + val exception = Exception("forced exception h4sab92yy8") + val querySlot = slot>() + val dataConnect = dataConnectWithQueryResult(Result.failure(exception), querySlot) + val queryRefImpl = Arb.queryRefImpl().next().copy(dataConnect = dataConnect) + + val thrownException = shouldThrow { queryRefImpl.execute() } + + assertSoftly { + thrownException shouldBeSameInstanceAs exception + querySlot.captured shouldBeSameInstanceAs queryRefImpl + } + } + + @Test + fun `subscribe() should return a QuerySubscription`() = runTest { + val queryRefImpl = Arb.queryRefImpl().next() + + val querySubscription = queryRefImpl.subscribe() + + querySubscription.query shouldBeSameInstanceAs queryRefImpl + } + + @Test + fun `subscribe() should always return a new object`() = runTest { + val queryRefImpl = Arb.queryRefImpl().next() + + val querySubscription1 = queryRefImpl.subscribe() + val querySubscription2 = queryRefImpl.subscribe() + + querySubscription1 shouldNotBeSameInstanceAs querySubscription2 + } + + @Test + fun `constructor assigns public properties to the given arguments`() { + val values = Arb.queryRefImpl().next() + val queryRefImpl = + QueryRefImpl( + dataConnect = values.dataConnect, + operationName = values.operationName, + variables = values.variables, + dataDeserializer = values.dataDeserializer, + variablesSerializer = values.variablesSerializer, + isFromGeneratedSdk = values.isFromGeneratedSdk, + ) + + queryRefImpl.asClue { + assertSoftly { + it.dataConnect shouldBeSameInstanceAs values.dataConnect + it.operationName shouldBeSameInstanceAs values.operationName + it.variables shouldBeSameInstanceAs values.variables + it.dataDeserializer shouldBeSameInstanceAs values.dataDeserializer + it.variablesSerializer shouldBeSameInstanceAs values.variablesSerializer + it.isFromGeneratedSdk shouldBe values.isFromGeneratedSdk + } + } + } + + @Test + fun `hashCode() should return the same value when invoked repeatedly`() { + val queryRefImpl: QueryRefImpl<*, *> = Arb.queryRefImpl().next() + val hashCode = queryRefImpl.hashCode() + repeat(10) { queryRefImpl.hashCode() shouldBe hashCode } + } + + @Test + fun `hashCode() should return the same value when invoked on distinct, but equal, objects`() { + val queryRefImpl1: QueryRefImpl<*, *> = Arb.queryRefImpl().next() + val queryRefImpl2: QueryRefImpl<*, *> = queryRefImpl1.copy() + queryRefImpl1 shouldNotBeSameInstanceAs queryRefImpl2 // verify test precondition + repeat(10) { queryRefImpl1.hashCode() shouldBe queryRefImpl2.hashCode() } + } + + @Test + fun `hashCode() should incorporate dataConnect`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(dataConnect = mockk(name = stringArb.next())) } + } + + @Test + fun `hashCode() should incorporate operationName`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(operationName = stringArb.next()) } + } + + @Test + fun `hashCode() should incorporate variables`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(variables = TestVariables(stringArb.next())) } + } + + @Test + fun `hashCode() should incorporate dataDeserializer`() = runTest { + verifyHashCodeEventuallyDiffers { it.copy(dataDeserializer = mockk(name = stringArb.next())) } + } + + @Test + fun `hashCode() should incorporate variablesSerializer`() = runTest { + verifyHashCodeEventuallyDiffers { + it.copy(variablesSerializer = mockk(name = stringArb.next())) + } + } + + @Test + fun `hashCode() should NOT incorporate isFromGeneratedSdk`() = runTest { + val queryRef1 = Arb.queryRefImpl().next() + val queryRef2 = queryRef1.copy(isFromGeneratedSdk = !queryRef1.isFromGeneratedSdk) + queryRef1.hashCode() shouldBe queryRef2.hashCode() + } + + private suspend fun verifyHashCodeEventuallyDiffers( + otherFactory: + (other: QueryRefImpl) -> QueryRefImpl + ) { + val obj1: QueryRefImpl = Arb.queryRefImpl().next() + retry(maxRetry = 50, timeout = Duration.INFINITE) { + val obj2: QueryRefImpl = otherFactory(obj1) + obj1.hashCode() shouldNotBe obj2.hashCode() + } + } + + @Test + fun `equals(this) should return true`() = runTest { + val queryRefImpl: QueryRefImpl = Arb.queryRefImpl().next() + queryRefImpl.equals(queryRefImpl) shouldBe true + } + + @Test + fun `equals(equal, but distinct, instance) should return true`() = runTest { + val queryRefImpl1: QueryRefImpl = Arb.queryRefImpl().next() + val queryRefImpl2: QueryRefImpl = queryRefImpl1.copy() + queryRefImpl1 shouldNotBeSameInstanceAs queryRefImpl2 // verify test precondition + queryRefImpl1.equals(queryRefImpl2) shouldBe true + } + + @Test + fun `equals(null) should return false`() = runTest { + val queryRefImpl: QueryRefImpl = Arb.queryRefImpl().next() + queryRefImpl.equals(null) shouldBe false + } + + @Test + fun `equals(an object of a different type) should return false`() = runTest { + val queryRefImpl: QueryRefImpl = Arb.queryRefImpl().next() + queryRefImpl.equals("not a QueryRefImpl") shouldBe false + } + + @Test + fun `equals() should return false when only dataConnect differs`() = runTest { + val queryRefImpl1: QueryRefImpl = Arb.queryRefImpl().next() + val queryRefImpl2 = queryRefImpl1.copy(dataConnect = mockk(stringArb.next())) + queryRefImpl1.equals(queryRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only operationName differs`() = runTest { + val queryRefImpl1: QueryRefImpl = Arb.queryRefImpl().next() + val queryRefImpl2 = queryRefImpl1.copy(operationName = queryRefImpl1.operationName + "2") + queryRefImpl1.equals(queryRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only variables differs`() = runTest { + val queryRefImpl1: QueryRefImpl = Arb.queryRefImpl().next() + val queryRefImpl2 = + queryRefImpl1.copy(variables = TestVariables(queryRefImpl1.variables.bar + "2")) + queryRefImpl1.equals(queryRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only dataDeserializer differs`() = runTest { + val queryRefImpl1: QueryRefImpl = Arb.queryRefImpl().next() + val queryRefImpl2 = queryRefImpl1.copy(dataDeserializer = mockk(stringArb.next())) + queryRefImpl1.equals(queryRefImpl2) shouldBe false + } + + @Test + fun `equals() should return false when only variablesSerializer differs`() = runTest { + val queryRefImpl1: QueryRefImpl = Arb.queryRefImpl().next() + val queryRefImpl2 = queryRefImpl1.copy(variablesSerializer = mockk(stringArb.next())) + queryRefImpl1.equals(queryRefImpl2) shouldBe false + } + + @Test + fun `equals() should return _TRUE_ when only isFromGeneratedSdk differs`() = runTest { + val queryRefImpl1: QueryRefImpl = Arb.queryRefImpl().next() + val queryRefImpl2 = queryRefImpl1.copy(isFromGeneratedSdk = !queryRefImpl1.isFromGeneratedSdk) + queryRefImpl1.equals(queryRefImpl2) shouldBe true + } + + @Test + fun `toString() should incorporate the string representations of public properties`() = runTest { + val queryRefImpl: QueryRefImpl = Arb.queryRefImpl().next() + val queryRefImpls = + listOf( + queryRefImpl, + queryRefImpl.copy(isFromGeneratedSdk = !queryRefImpl.isFromGeneratedSdk), + ) + val toStringResult = queryRefImpl.toString() + + assertSoftly { + queryRefImpls.forEach { + it.asClue { + toStringResult.shouldContain("dataConnect=${queryRefImpl.dataConnect}") + toStringResult.shouldContain("operationName=${queryRefImpl.operationName}") + toStringResult.shouldContain("variables=${queryRefImpl.variables}") + toStringResult.shouldContain("dataDeserializer=${queryRefImpl.dataDeserializer}") + toStringResult.shouldContain("variablesSerializer=${queryRefImpl.variablesSerializer}") + } + } + } + } + + private companion object { + val stringArb = Arb.string(6, codepoints = Codepoint.alphanumeric()) + + fun Arb.Companion.testVariables(): Arb = arbitrary { + val stringArb = Arb.string(6, Codepoint.alphanumeric()) + TestVariables(stringArb.bind()) + } + + fun Arb.Companion.queryRefImpl(): Arb> = + queryRefImpl(Arb.testVariables()) + + fun dataConnectWithQueryResult( + result: Result, + querySlot: CapturingSlot> + ): FirebaseDataConnectInternal = + mockk(relaxed = true) { + every { lazyQueryManager } returns + SuspendingLazy { + mockk { + coEvery { execute(capture(querySlot)) } returns SequencedReference(123, result) + } + } + } + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/Arbs.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/Arbs.kt new file mode 100644 index 00000000000..5e46761beca --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/Arbs.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("InternalArbs") + +package com.google.firebase.dataconnect.testutil + +import com.google.firebase.dataconnect.DataConnectError +import com.google.firebase.dataconnect.core.DataConnectGrpcClient +import com.google.firebase.dataconnect.core.MutationRefImpl +import com.google.firebase.dataconnect.core.QueryRefImpl +import com.google.firebase.dataconnect.util.toStructProto +import io.kotest.property.Arb +import io.kotest.property.arbitrary.Codepoint +import io.kotest.property.arbitrary.alphanumeric +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.orNull +import io.kotest.property.arbitrary.positiveInt +import io.kotest.property.arbitrary.string +import io.kotest.property.arbs.firstName +import io.mockk.mockk + +internal fun Arb.Companion.dataConnectErrorSourceLocation(): Arb = + arbitrary { + val line = Arb.int(1..9999).bind() + val column = Arb.int(1..9999).bind() + DataConnectError.SourceLocation(line = line, column = column) + } + +internal fun Arb.Companion.dataConnectError(): Arb = arbitrary { + val message = "sx7s673h4n_" + Arb.string(20, codepoints = Codepoint.alphanumeric()).bind() + val numPathSegments = Arb.int(1..3).bind() + val path = List(numPathSegments) { Arb.dataConnectErrorPathSegment().bind() } + val numLocations = Arb.int(0..3).bind() + val locations = List(numLocations) { Arb.dataConnectErrorSourceLocation().bind() } + DataConnectError( + message = message, + path = path, + locations = locations, + ) +} + +internal fun Arb.Companion.dataConnectErrorPathSegmentField(): + Arb = arbitrary { + DataConnectError.PathSegment.Field(Arb.firstName().bind().name) +} + +internal fun Arb.Companion.dataConnectErrorPathSegmentListIndex(): + Arb = arbitrary { + DataConnectError.PathSegment.ListIndex(Arb.positiveInt().bind()) +} + +internal fun Arb.Companion.dataConnectErrorPathSegment(): Arb = + arbitrary { + if (Arb.boolean().bind()) { + Arb.dataConnectErrorPathSegmentField().bind() + } else { + Arb.dataConnectErrorPathSegmentListIndex().bind() + } + } + +internal fun Arb.Companion.operationResult() = arbitrary { + val data = Arb.anyMapScalar().orNull(nullProbability = 0.1).bind()?.toStructProto() + val numErrors = Arb.int(0..3).bind() + val errors = List(numErrors) { Arb.dataConnectError().bind() } + DataConnectGrpcClient.OperationResult(data, errors) +} + +internal fun Arb.Companion.queryRefImpl( + variablesArb: Arb +): Arb> = arbitrary { + val stringArb = Arb.string(6, codepoints = Codepoint.alphanumeric()) + QueryRefImpl( + dataConnect = mockk(stringArb.bind()), + operationName = stringArb.bind(), + variables = variablesArb.bind(), + dataDeserializer = mockk(stringArb.bind()), + variablesSerializer = mockk(stringArb.bind()), + isFromGeneratedSdk = boolean().bind(), + ) +} + +internal fun Arb.Companion.mutationRefImpl( + variablesArb: Arb +): Arb> = arbitrary { + val stringArb = Arb.string(6, codepoints = Codepoint.alphanumeric()) + MutationRefImpl( + dataConnect = mockk(stringArb.bind()), + operationName = stringArb.bind(), + variables = variablesArb.bind(), + dataDeserializer = mockk(stringArb.bind()), + variablesSerializer = mockk(stringArb.bind()), + isFromGeneratedSdk = boolean().bind(), + ) +} + +internal fun Arb.Companion.operationRefImpl( + variablesArb: Arb +): Arb> = arbitrary { + val stringArb = Arb.string(6, codepoints = Codepoint.alphanumeric()) + StubOperationRefImpl( + dataConnect = mockk(stringArb.bind()), + operationName = stringArb.bind(), + variables = variablesArb.bind(), + dataDeserializer = mockk(stringArb.bind()), + variablesSerializer = mockk(stringArb.bind()), + ) +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/Stubs.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/Stubs.kt new file mode 100644 index 00000000000..cbda966bcd6 --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/Stubs.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.firebase.dataconnect.testutil + +import com.google.firebase.dataconnect.core.FirebaseDataConnectInternal +import com.google.firebase.dataconnect.core.OperationRefImpl +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationStrategy + +internal class StubOperationRefImpl( + dataConnect: FirebaseDataConnectInternal, + operationName: String, + variables: Variables, + dataDeserializer: DeserializationStrategy, + variablesSerializer: SerializationStrategy, +) : + OperationRefImpl( + dataConnect = dataConnect, + operationName = operationName, + variables = variables, + dataDeserializer = dataDeserializer, + variablesSerializer = variablesSerializer, + ) { + override suspend fun execute(): OperationResultImpl { + throw UnsupportedOperationException("this stub method is not supported") + } +} + +internal fun StubOperationRefImpl.copy( + dataConnect: FirebaseDataConnectInternal = this.dataConnect, + operationName: String = this.operationName, + variables: Variables = this.variables, + dataDeserializer: DeserializationStrategy = this.dataDeserializer, + variablesSerializer: SerializationStrategy = this.variablesSerializer, +): StubOperationRefImpl = + StubOperationRefImpl( + dataConnect = dataConnect, + operationName = operationName, + variables = variables, + dataDeserializer = dataDeserializer, + variablesSerializer = variablesSerializer, + ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 98a32cf1070..9039a814e4e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -82,11 +82,12 @@ protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", versio kotest-runner = { module = "io.kotest:kotest-runner-junit4-jvm", version.ref = "kotest" } kotest-assertions = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest" } kotest-property = { module = "io.kotest:kotest-property-jvm", version.ref = "kotest" } +kotest-property-arbs = { module = "io.kotest.extensions:kotest-property-arbs", version = "2.1.2" } quickcheck = { module = "net.java:quickcheck", version.ref = "quickcheck" } turbine = { module = "app.cash.turbine:turbine", version = "1.0.0" } [bundles] -kotest = ["kotest-runner", "kotest-assertions", "kotest-property"] +kotest = ["kotest-runner", "kotest-assertions", "kotest-property", "kotest-property-arbs"] playservices = ["playservices-base", "playservices-basement", "playservices-tasks"] [plugins]