16
16
package com.google.firebase.dataconnect.core
17
17
18
18
import com.google.firebase.dataconnect.DataConnectError
19
+ import com.google.firebase.dataconnect.DataConnectException
20
+ import com.google.firebase.dataconnect.DataConnectUntypedData
19
21
import com.google.firebase.dataconnect.core.DataConnectGrpcClient.OperationResult
20
22
import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule
21
23
import com.google.firebase.dataconnect.testutil.connectorConfig
24
+ import com.google.firebase.dataconnect.testutil.dataConnectError
22
25
import com.google.firebase.dataconnect.testutil.iterator
23
26
import com.google.firebase.dataconnect.testutil.newMockLogger
24
27
import com.google.firebase.dataconnect.testutil.operationName
28
+ import com.google.firebase.dataconnect.testutil.operationResult
25
29
import com.google.firebase.dataconnect.testutil.projectId
26
30
import com.google.firebase.dataconnect.testutil.requestId
27
31
import com.google.firebase.dataconnect.testutil.shouldHaveLoggedExactlyOneMessageContaining
28
32
import com.google.firebase.dataconnect.util.buildStructProto
33
+ import com.google.firebase.dataconnect.util.encodeToStruct
34
+ import com.google.firebase.dataconnect.util.toMap
29
35
import com.google.protobuf.ListValue
30
36
import com.google.protobuf.Value
31
37
import google.firebase.dataconnect.proto.ExecuteMutationRequest
@@ -36,24 +42,38 @@ import google.firebase.dataconnect.proto.GraphqlError
36
42
import google.firebase.dataconnect.proto.SourceLocation
37
43
import io.grpc.Status
38
44
import io.grpc.StatusException
45
+ import io.kotest.assertions.asClue
39
46
import io.kotest.assertions.throwables.shouldThrow
47
+ import io.kotest.matchers.collections.shouldContainExactly
48
+ import io.kotest.matchers.nulls.shouldBeNull
40
49
import io.kotest.matchers.shouldBe
50
+ import io.kotest.matchers.string.shouldContain
41
51
import io.kotest.matchers.types.shouldBeSameInstanceAs
42
52
import io.kotest.property.Arb
43
53
import io.kotest.property.RandomSource
44
54
import io.kotest.property.arbitrary.Codepoint
45
55
import io.kotest.property.arbitrary.alphanumeric
46
56
import io.kotest.property.arbitrary.boolean
47
57
import io.kotest.property.arbitrary.egyptianHieroglyphs
58
+ import io.kotest.property.arbitrary.filter
48
59
import io.kotest.property.arbitrary.int
60
+ import io.kotest.property.arbitrary.map
49
61
import io.kotest.property.arbitrary.merge
50
62
import io.kotest.property.arbitrary.next
51
63
import io.kotest.property.arbitrary.string
64
+ import io.kotest.property.arbs.firstName
65
+ import io.kotest.property.arbs.travel.airline
66
+ import io.kotest.property.checkAll
52
67
import io.mockk.coEvery
53
68
import io.mockk.coVerify
69
+ import io.mockk.every
54
70
import io.mockk.mockk
71
+ import io.mockk.spyk
55
72
import java.util.concurrent.atomic.AtomicBoolean
56
73
import kotlinx.coroutines.test.runTest
74
+ import kotlinx.serialization.DeserializationStrategy
75
+ import kotlinx.serialization.Serializable
76
+ import kotlinx.serialization.serializer
57
77
import org.junit.Rule
58
78
import org.junit.Test
59
79
@@ -584,3 +604,107 @@ class DataConnectGrpcClientUnitTest {
584
604
}
585
605
}
586
606
}
607
+
608
+ @Suppress(" IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE" )
609
+ class DataConnectGrpcClientOperationResultUnitTest {
610
+
611
+ @Test
612
+ fun `deserialize() should ignore the module given with DataConnectUntypedData` () {
613
+ val errors = listOf (Arb .dataConnectError().next())
614
+ val operationResult = OperationResult (buildStructProto { put(" foo" , 42.0 ) }, errors)
615
+ val result = operationResult.deserialize(DataConnectUntypedData )
616
+ result shouldBe DataConnectUntypedData (mapOf (" foo" to 42.0 ), errors)
617
+ }
618
+
619
+ @Test
620
+ fun `deserialize() should treat DataConnectUntypedData specially` () = runTest {
621
+ checkAll(iterations = 1000 , Arb .operationResult()) { operationResult ->
622
+ val result = operationResult.deserialize(DataConnectUntypedData )
623
+
624
+ result.asClue {
625
+ if (operationResult.data == = null ) {
626
+ it.data.shouldBeNull()
627
+ } else {
628
+ it.data shouldBe operationResult.data.toMap()
629
+ }
630
+ it.errors shouldContainExactly operationResult.errors
631
+ }
632
+ }
633
+ }
634
+
635
+ @Test
636
+ fun `deserialize() should throw if one or more errors and data is null` () = runTest {
637
+ val arb = Arb .operationResult().filter { it.errors.isNotEmpty() }.map { it.copy(data = null ) }
638
+ checkAll(iterations = 5 , arb) { operationResult ->
639
+ val exception =
640
+ shouldThrow<DataConnectException > { operationResult.deserialize<Nothing >(mockk()) }
641
+ exception.message shouldContain " ${operationResult.errors} "
642
+ }
643
+ }
644
+
645
+ @Test
646
+ fun `deserialize() should throw if one or more errors and data is _not_ null` () = runTest {
647
+ val arb = Arb .operationResult().filter { it.data != = null && it.errors.isNotEmpty() }
648
+ checkAll(iterations = 5 , arb) { operationResult ->
649
+ val exception =
650
+ shouldThrow<DataConnectException > { operationResult.deserialize<Nothing >(mockk()) }
651
+ exception.message shouldContain " ${operationResult.errors} "
652
+ }
653
+ }
654
+
655
+ @Test
656
+ fun `deserialize() should throw if data is null and errors is empty` () {
657
+ val operationResult = OperationResult (data = null , errors = emptyList())
658
+ val exception =
659
+ shouldThrow<DataConnectException > { operationResult.deserialize<Nothing >(mockk()) }
660
+ exception.message shouldContain " no data"
661
+ }
662
+
663
+ @Test
664
+ fun `deserialize() successfully deserializes` () = runTest {
665
+ val testData = TestData (Arb .firstName().next().name)
666
+ val operationResult = OperationResult (encodeToStruct(testData), errors = emptyList())
667
+
668
+ val deserializedData = operationResult.deserialize(serializer<TestData >())
669
+
670
+ deserializedData shouldBe testData
671
+ }
672
+
673
+ @Test
674
+ fun `deserialize() throws if decoding fails` () = runTest {
675
+ val data = buildStructProto { put(" zzzz" , 42 ) }
676
+ val operationResult = OperationResult (data, errors = emptyList())
677
+ shouldThrow<DataConnectException > { operationResult.deserialize(serializer<TestData >()) }
678
+ }
679
+
680
+ @Test
681
+ fun `deserialize() re-throws DataConnectException` () = runTest {
682
+ val data = encodeToStruct(TestData (" fe45zhyd3m" ))
683
+ val operationResult = OperationResult (data = data, errors = emptyList())
684
+ val deserializer: DeserializationStrategy <TestData > = spyk(serializer())
685
+ val exception = DataConnectException (message = Arb .airline().next().name)
686
+ every { deserializer.deserialize(any()) } throws (exception)
687
+
688
+ val thrownException =
689
+ shouldThrow<DataConnectException > { operationResult.deserialize(deserializer) }
690
+
691
+ thrownException shouldBeSameInstanceAs exception
692
+ }
693
+
694
+ @Test
695
+ fun `deserialize() wraps non-DataConnectException in DataConnectException` () = runTest {
696
+ val data = encodeToStruct(TestData (" rbmkny6b4r" ))
697
+ val operationResult = OperationResult (data = data, errors = emptyList())
698
+ val deserializer: DeserializationStrategy <TestData > = spyk(serializer())
699
+ class MyException : Exception (" y3cx44q43q" )
700
+ val exception = MyException ()
701
+ every { deserializer.deserialize(any()) } throws (exception)
702
+
703
+ val thrownException =
704
+ shouldThrow<DataConnectException > { operationResult.deserialize(deserializer) }
705
+
706
+ thrownException.cause shouldBeSameInstanceAs exception
707
+ }
708
+
709
+ @Serializable data class TestData (val foo : String )
710
+ }
0 commit comments