Skip to content

Add a bunch of unit test coverage for mutations and queries #6296

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions firebase-dataconnect/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ package com.google.firebase.dataconnect.core {
public final class LoggerKt {
}

public final class MutationRefImplKt {
}

public final class QueryRefImplKt {
}

Expand Down
2 changes: 2 additions & 0 deletions firebase-dataconnect/firebase-dataconnect.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,45 @@ internal class MutationRefImpl<Data, Variables>(
override fun toString() = "MutationResultImpl(data=$data, ref=$ref)"
}
}

internal fun <Data, Variables> MutationRefImpl<Data, Variables>.copy(
dataConnect: FirebaseDataConnectInternal = this.dataConnect,
operationName: String = this.operationName,
variables: Variables = this.variables,
dataDeserializer: DeserializationStrategy<Data> = this.dataDeserializer,
variablesSerializer: SerializationStrategy<Variables> = this.variablesSerializer,
isFromGeneratedSdk: Boolean = this.isFromGeneratedSdk,
) =
MutationRefImpl(
dataConnect = dataConnect,
operationName = operationName,
variables = variables,
dataDeserializer = dataDeserializer,
variablesSerializer = variablesSerializer,
isFromGeneratedSdk = isFromGeneratedSdk,
)

internal fun <Data, NewVariables> MutationRefImpl<Data, *>.withVariablesSerializer(
variables: NewVariables,
variablesSerializer: SerializationStrategy<NewVariables>,
): MutationRefImpl<Data, NewVariables> =
MutationRefImpl(
dataConnect = dataConnect,
operationName = operationName,
variables = variables,
dataDeserializer = dataDeserializer,
variablesSerializer = variablesSerializer,
isFromGeneratedSdk = isFromGeneratedSdk,
)

internal fun <NewData, Variables> MutationRefImpl<*, Variables>.withDataDeserializer(
dataDeserializer: DeserializationStrategy<NewData>,
): MutationRefImpl<NewData, Variables> =
MutationRefImpl(
dataConnect = dataConnect,
operationName = operationName,
variables = variables,
dataDeserializer = dataDeserializer,
variablesSerializer = variablesSerializer,
isFromGeneratedSdk = isFromGeneratedSdk,
)
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,19 @@ internal class QueryRefImpl<Data, Variables>(
}
}

internal fun <Data, Variables> QueryRefImpl<Data, Variables>.withVariables(variables: Variables) =
internal fun <Data, Variables> QueryRefImpl<Data, Variables>.copy(
dataConnect: FirebaseDataConnectInternal = this.dataConnect,
operationName: String = this.operationName,
variables: Variables = this.variables,
dataDeserializer: DeserializationStrategy<Data> = this.dataDeserializer,
variablesSerializer: SerializationStrategy<Variables> = 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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ internal class QuerySubscriptionImpl<Data, Variables>(query: QueryRefImpl<Data,
}

override suspend fun update(variables: Variables) {
_query.value = _query.value.withVariables(variables)
_query.value = _query.value.copy(variables = variables)
reload()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,22 @@
package com.google.firebase.dataconnect.core

import com.google.firebase.dataconnect.DataConnectError
import com.google.firebase.dataconnect.DataConnectException
import com.google.firebase.dataconnect.DataConnectUntypedData
import com.google.firebase.dataconnect.core.DataConnectGrpcClient.OperationResult
import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule
import com.google.firebase.dataconnect.testutil.connectorConfig
import com.google.firebase.dataconnect.testutil.dataConnectError
import com.google.firebase.dataconnect.testutil.iterator
import com.google.firebase.dataconnect.testutil.newMockLogger
import com.google.firebase.dataconnect.testutil.operationName
import com.google.firebase.dataconnect.testutil.operationResult
import com.google.firebase.dataconnect.testutil.projectId
import com.google.firebase.dataconnect.testutil.requestId
import com.google.firebase.dataconnect.testutil.shouldHaveLoggedExactlyOneMessageContaining
import com.google.firebase.dataconnect.util.buildStructProto
import com.google.firebase.dataconnect.util.encodeToStruct
import com.google.firebase.dataconnect.util.toMap
import com.google.protobuf.ListValue
import com.google.protobuf.Value
import google.firebase.dataconnect.proto.ExecuteMutationRequest
Expand All @@ -36,24 +42,38 @@ import google.firebase.dataconnect.proto.GraphqlError
import google.firebase.dataconnect.proto.SourceLocation
import io.grpc.Status
import io.grpc.StatusException
import io.kotest.assertions.asClue
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.types.shouldBeSameInstanceAs
import io.kotest.property.Arb
import io.kotest.property.RandomSource
import io.kotest.property.arbitrary.Codepoint
import io.kotest.property.arbitrary.alphanumeric
import io.kotest.property.arbitrary.boolean
import io.kotest.property.arbitrary.egyptianHieroglyphs
import io.kotest.property.arbitrary.filter
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.string
import io.kotest.property.arbs.firstName
import io.kotest.property.arbs.travel.airline
import io.kotest.property.checkAll
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import org.junit.Rule
import org.junit.Test

Expand Down Expand Up @@ -584,3 +604,107 @@ class DataConnectGrpcClientUnitTest {
}
}
}

@Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE")
class DataConnectGrpcClientOperationResultUnitTest {

@Test
fun `deserialize() should ignore the module given with DataConnectUntypedData`() {
val errors = listOf(Arb.dataConnectError().next())
val operationResult = OperationResult(buildStructProto { put("foo", 42.0) }, errors)
val result = operationResult.deserialize(DataConnectUntypedData)
result shouldBe DataConnectUntypedData(mapOf("foo" to 42.0), errors)
}

@Test
fun `deserialize() should treat DataConnectUntypedData specially`() = runTest {
checkAll(iterations = 1000, Arb.operationResult()) { operationResult ->
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<DataConnectException> { operationResult.deserialize<Nothing>(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<DataConnectException> { operationResult.deserialize<Nothing>(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<DataConnectException> { operationResult.deserialize<Nothing>(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<TestData>())

deserializedData shouldBe testData
}

@Test
fun `deserialize() throws if decoding fails`() = runTest {
val data = buildStructProto { put("zzzz", 42) }
val operationResult = OperationResult(data, errors = emptyList())
shouldThrow<DataConnectException> { operationResult.deserialize(serializer<TestData>()) }
}

@Test
fun `deserialize() re-throws DataConnectException`() = runTest {
val data = encodeToStruct(TestData("fe45zhyd3m"))
val operationResult = OperationResult(data = data, errors = emptyList())
val deserializer: DeserializationStrategy<TestData> = spyk(serializer())
val exception = DataConnectException(message = Arb.airline().next().name)
every { deserializer.deserialize(any()) } throws (exception)

val thrownException =
shouldThrow<DataConnectException> { 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<TestData> = spyk(serializer())
class MyException : Exception("y3cx44q43q")
val exception = MyException()
every { deserializer.deserialize(any()) } throws (exception)

val thrownException =
shouldThrow<DataConnectException> { operationResult.deserialize(deserializer) }

thrownException.cause shouldBeSameInstanceAs exception
}

@Serializable data class TestData(val foo: String)
}
Loading
Loading