Skip to content

Commit 14d0c5b

Browse files
committed
OperationExecutionErrorsIntegrationTest.kt added
1 parent f51a55a commit 14d0c5b

File tree

5 files changed

+367
-36
lines changed

5 files changed

+367
-36
lines changed

firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,14 @@ query getPersonAuth($id: String!) @auth(level: USER_ANON) {
8888
age
8989
}
9090
}
91+
92+
query getPersonWithPartialFailure($id: String!) @auth(level: PUBLIC) {
93+
person1: person(id: $id) { name }
94+
person2: person(id: $id) @check(expr: "false", message: "c8azjdwz2x") { name }
95+
}
96+
97+
mutation createPersonWithPartialFailure($id: String!, $name: String!) @auth(level: PUBLIC) {
98+
person1: person_insert(data: { id: $id, name: $name })
99+
person2: person_insert(data: { id_expr: "uuidV4()", name: $name }) @check(expr: "false", message: "ecxpjy4qfy")
100+
}
101+
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.dataconnect
18+
19+
import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase
20+
import com.google.firebase.dataconnect.testutil.schemas.PersonSchema
21+
import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation
22+
import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery
23+
import com.google.firebase.dataconnect.testutil.shouldSatisfy
24+
import io.kotest.assertions.throwables.shouldThrow
25+
import io.kotest.matchers.collections.shouldHaveAtLeastSize
26+
import io.kotest.property.Arb
27+
import io.kotest.property.arbitrary.map
28+
import io.kotest.property.arbitrary.next
29+
import kotlinx.coroutines.test.runTest
30+
import kotlinx.serialization.Serializable
31+
import kotlinx.serialization.SerializationException
32+
import kotlinx.serialization.serializer
33+
import org.junit.Test
34+
35+
class OperationExecutionErrorsIntegrationTest : DataConnectIntegrationTestBase() {
36+
37+
private val personSchema: PersonSchema by lazy { PersonSchema(dataConnectFactory) }
38+
private val dataConnect: FirebaseDataConnect by lazy { personSchema.dataConnect }
39+
40+
@Test
41+
fun executeQueryFailsWithNullDataNonEmptyErrors() = runTest {
42+
val queryRef =
43+
dataConnect.query(
44+
operationName = GetPersonQuery.operationName,
45+
variables = Arb.incompatibleVariables().next(rs),
46+
dataDeserializer = serializer<GetPersonQuery.Data>(),
47+
variablesSerializer = serializer(),
48+
optionsBuilder = {},
49+
)
50+
51+
val exception = shouldThrow<DataConnectOperationException> { queryRef.execute() }
52+
53+
exception.shouldSatisfy(
54+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
55+
expectedMessageSubstringCaseSensitive = "jwdbzka4k5",
56+
expectedCause = null,
57+
expectedRawData = null,
58+
expectedData = null,
59+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
60+
)
61+
}
62+
63+
@Test
64+
fun executeMutationFailsWithNullDataNonEmptyErrors() = runTest {
65+
val mutationRef =
66+
dataConnect.mutation(
67+
operationName = CreatePersonMutation.operationName,
68+
variables = Arb.incompatibleVariables().next(rs),
69+
dataDeserializer = serializer<CreatePersonMutation.Data>(),
70+
variablesSerializer = serializer(),
71+
optionsBuilder = {},
72+
)
73+
74+
val exception = shouldThrow<DataConnectOperationException> { mutationRef.execute() }
75+
76+
exception.shouldSatisfy(
77+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
78+
expectedCause = null,
79+
expectedRawData = null,
80+
expectedData = null,
81+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
82+
)
83+
}
84+
85+
@Test
86+
fun executeQueryFailsWithNonNullDataEmptyErrorsButDecodingResponseDataFails() = runTest {
87+
val id = Arb.alphanumericString().next()
88+
val queryRef =
89+
dataConnect.query(
90+
operationName = GetPersonQuery.operationName,
91+
variables = GetPersonQuery.Variables(id),
92+
dataDeserializer = serializer<IncompatibleData>(),
93+
variablesSerializer = serializer(),
94+
optionsBuilder = {},
95+
)
96+
97+
val exception = shouldThrow<DataConnectOperationException> { queryRef.execute() }
98+
99+
exception.shouldSatisfy(
100+
expectedMessageSubstringCaseInsensitive = "decoding data from the server's response failed",
101+
expectedCause = SerializationException::class,
102+
expectedRawData = mapOf("person" to null),
103+
expectedData = null,
104+
expectedErrors = emptyList(),
105+
)
106+
}
107+
108+
@Test
109+
fun executeMutationFailsWithNonNullDataEmptyErrorsButDecodingResponseDataFails() = runTest {
110+
val id = Arb.alphanumericString().next()
111+
val name = Arb.alphanumericString().next()
112+
val mutationRef =
113+
dataConnect.mutation(
114+
operationName = CreatePersonMutation.operationName,
115+
variables = CreatePersonMutation.Variables(id, name),
116+
dataDeserializer = serializer<IncompatibleData>(),
117+
variablesSerializer = serializer(),
118+
optionsBuilder = {},
119+
)
120+
121+
val exception = shouldThrow<DataConnectOperationException> { mutationRef.execute() }
122+
123+
exception.shouldSatisfy(
124+
expectedMessageSubstringCaseInsensitive = "decoding data from the server's response failed",
125+
expectedCause = SerializationException::class,
126+
expectedRawData = mapOf("person_insert" to mapOf("id" to id)),
127+
expectedData = null,
128+
expectedErrors = emptyList(),
129+
)
130+
}
131+
132+
@Test
133+
fun executeQueryFailsWithNonNullDataNonEmptyErrorsDecodingSucceeds() = runTest {
134+
val id = Arb.alphanumericString().next()
135+
val name = Arb.alphanumericString().next()
136+
personSchema.createPerson(CreatePersonMutation.Variables(id, name)).execute()
137+
val queryRef =
138+
dataConnect.query(
139+
operationName = "getPersonWithPartialFailure",
140+
variables = GetPersonWithPartialFailureVariables(id),
141+
dataDeserializer = serializer<GetPersonWithPartialFailureData>(),
142+
variablesSerializer = serializer(),
143+
optionsBuilder = {},
144+
)
145+
146+
val exception = shouldThrow<DataConnectOperationException> { queryRef.execute() }
147+
148+
exception.shouldSatisfy(
149+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
150+
expectedMessageSubstringCaseSensitive = "c8azjdwz2x",
151+
expectedCause = null,
152+
expectedRawData = mapOf("person1" to mapOf("name" to name), "person2" to null),
153+
expectedData = GetPersonWithPartialFailureData(name),
154+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
155+
)
156+
}
157+
158+
@Test
159+
fun executeMutationFailsWithNonNullDataNonEmptyErrorsDecodingSucceeds() = runTest {
160+
val id = Arb.alphanumericString().next()
161+
val name = Arb.alphanumericString().next()
162+
val mutationRef =
163+
dataConnect.mutation(
164+
operationName = "createPersonWithPartialFailure",
165+
variables = CreatePersonWithPartialFailureVariables(id = id, name = name),
166+
dataDeserializer = serializer<CreatePersonWithPartialFailureData>(),
167+
variablesSerializer = serializer(),
168+
optionsBuilder = {},
169+
)
170+
171+
val exception = shouldThrow<DataConnectOperationException> { mutationRef.execute() }
172+
173+
exception.shouldSatisfy(
174+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
175+
expectedMessageSubstringCaseSensitive = "ecxpjy4qfy",
176+
expectedCause = null,
177+
expectedRawData = mapOf("person1" to mapOf("id" to id), "person2" to null),
178+
expectedData = CreatePersonWithPartialFailureData(id),
179+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
180+
)
181+
}
182+
183+
@Test
184+
fun executeQueryFailsWithNonNullDataNonEmptyErrorsDecodingFails() = runTest {
185+
val id = Arb.alphanumericString().next()
186+
val name = Arb.alphanumericString().next()
187+
personSchema.createPerson(CreatePersonMutation.Variables(id, name)).execute()
188+
val queryRef =
189+
dataConnect.query(
190+
operationName = "getPersonWithPartialFailure",
191+
variables = GetPersonWithPartialFailureVariables(id),
192+
dataDeserializer = serializer<IncompatibleData>(),
193+
variablesSerializer = serializer(),
194+
optionsBuilder = {},
195+
)
196+
197+
val exception = shouldThrow<DataConnectOperationException> { queryRef.execute() }
198+
199+
exception.shouldSatisfy(
200+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
201+
expectedMessageSubstringCaseSensitive = "c8azjdwz2x",
202+
expectedCause = null,
203+
expectedRawData = mapOf("person1" to mapOf("name" to name), "person2" to null),
204+
expectedData = null,
205+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
206+
)
207+
}
208+
209+
@Test
210+
fun executeMutationFailsWithNonNullDataNonEmptyErrorsDecodingFails() = runTest {
211+
val id = Arb.alphanumericString().next()
212+
val name = Arb.alphanumericString().next()
213+
val mutationRef =
214+
dataConnect.mutation(
215+
operationName = "createPersonWithPartialFailure",
216+
variables = CreatePersonWithPartialFailureVariables(id = id, name = name),
217+
dataDeserializer = serializer<IncompatibleData>(),
218+
variablesSerializer = serializer(),
219+
optionsBuilder = {},
220+
)
221+
222+
val exception = shouldThrow<DataConnectOperationException> { mutationRef.execute() }
223+
224+
exception.shouldSatisfy(
225+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
226+
expectedMessageSubstringCaseSensitive = "ecxpjy4qfy",
227+
expectedCause = null,
228+
expectedRawData = mapOf("person1" to mapOf("id" to id), "person2" to null),
229+
expectedData = null,
230+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
231+
)
232+
}
233+
234+
@Serializable private data class IncompatibleVariables(val jwdbzka4k5: String)
235+
236+
@Serializable private data class IncompatibleData(val btzjhbfz7h: String)
237+
238+
private fun Arb.Companion.incompatibleVariables(string: Arb<String> = Arb.alphanumericString()) =
239+
string.map { IncompatibleVariables(it) }
240+
241+
@Serializable private data class GetPersonWithPartialFailureVariables(val id: String)
242+
243+
@Serializable
244+
private data class GetPersonWithPartialFailureData(val person1: Person, val person2: Nothing?) {
245+
constructor(person1Name: String) : this(Person(person1Name), null)
246+
247+
@Serializable private data class Person(val name: String)
248+
}
249+
250+
@Serializable
251+
private data class CreatePersonWithPartialFailureVariables(val id: String, val name: String)
252+
253+
@Serializable
254+
private data class CreatePersonWithPartialFailureData(
255+
val person1: Person,
256+
val person2: Nothing?
257+
) {
258+
constructor(person1Id: String) : this(Person(person1Id), null)
259+
260+
@Serializable private data class Person(val id: String)
261+
}
262+
}

firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) {
5454
)
5555

5656
object CreatePersonMutation {
57+
const val operationName = "createPerson"
58+
5759
@Serializable
5860
data class Data(val person_insert: PersonKey) {
5961
@Serializable data class PersonKey(val id: String)
@@ -63,7 +65,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) {
6365

6466
fun createPerson(variables: CreatePersonMutation.Variables) =
6567
dataConnect.mutation(
66-
operationName = "createPerson",
68+
operationName = CreatePersonMutation.operationName,
6769
variables = variables,
6870
dataDeserializer = serializer<CreatePersonMutation.Data>(),
6971
variablesSerializer = serializer<CreatePersonMutation.Variables>(),
@@ -141,6 +143,8 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) {
141143
fun deletePerson(id: String) = deletePerson(DeletePersonMutation.Variables(id = id))
142144

143145
object GetPersonQuery {
146+
const val operationName = "getPerson"
147+
144148
@Serializable
145149
data class Data(val person: Person?) {
146150
@Serializable data class Person(val name: String, val age: Int? = null)
@@ -151,7 +155,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) {
151155

152156
fun getPerson(variables: GetPersonQuery.Variables) =
153157
dataConnect.query(
154-
operationName = "getPerson",
158+
operationName = GetPersonQuery.operationName,
155159
variables = variables,
156160
dataDeserializer = serializer<GetPersonQuery.Data>(),
157161
variablesSerializer = serializer<GetPersonQuery.Variables>(),

firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@ import com.google.firebase.dataconnect.testutil.property.arbitrary.iterator
3333
import com.google.firebase.dataconnect.testutil.property.arbitrary.operationErrors
3434
import com.google.firebase.dataconnect.testutil.property.arbitrary.proto
3535
import com.google.firebase.dataconnect.testutil.property.arbitrary.struct
36-
import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText
37-
import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingTextIgnoringCase
3836
import com.google.firebase.dataconnect.testutil.shouldHaveLoggedExactlyOneMessageContaining
37+
import com.google.firebase.dataconnect.testutil.shouldSatisfy
3938
import com.google.firebase.dataconnect.util.ProtoUtil.buildStructProto
4039
import com.google.firebase.dataconnect.util.ProtoUtil.encodeToStruct
4140
import com.google.firebase.dataconnect.util.ProtoUtil.toMap
@@ -51,7 +50,6 @@ import google.firebase.dataconnect.proto.SourceLocation
5150
import io.grpc.Status
5251
import io.grpc.StatusException
5352
import io.kotest.assertions.assertSoftly
54-
import io.kotest.assertions.fail
5553
import io.kotest.assertions.throwables.shouldThrow
5654
import io.kotest.assertions.withClue
5755
import io.kotest.common.ExperimentalKotest
@@ -741,37 +739,15 @@ class DataConnectGrpcClientOperationResultUnitTest {
741739
expectedRawData: Struct?,
742740
expectedData: T?,
743741
expectedErrors: List<ErrorInfo>,
744-
) {
745-
assertSoftly {
746-
withClue("exception.message") {
747-
message shouldContainWithNonAbuttingTextIgnoringCase
748-
expectedMessageSubstringCaseInsensitive
749-
if (expectedMessageSubstringCaseSensitive != null) {
750-
message shouldContainWithNonAbuttingText expectedMessageSubstringCaseSensitive
751-
}
752-
}
753-
withClue("exception.cause") {
754-
if (expectedCause == null) {
755-
cause.shouldBeNull()
756-
} else {
757-
val cause = cause.shouldNotBeNull()
758-
if (!expectedCause.isInstance(cause)) {
759-
fail(
760-
"cause was an instance of ${cause::class.qualifiedName}, " +
761-
"but expected it to be an instance of ${expectedCause.qualifiedName}"
762-
)
763-
}
764-
}
765-
}
766-
withClue("exception.response.rawData") {
767-
response.rawData shouldBe expectedRawData?.toMap()
768-
}
769-
withClue("exception.response.data") { response.data shouldBe expectedData }
770-
withClue("exception.response.errors") {
771-
response.errors shouldContainExactly expectedErrors
772-
}
773-
}
774-
}
742+
) =
743+
shouldSatisfy(
744+
expectedMessageSubstringCaseInsensitive = expectedMessageSubstringCaseInsensitive,
745+
expectedMessageSubstringCaseSensitive = expectedMessageSubstringCaseSensitive,
746+
expectedCause = expectedCause,
747+
expectedRawData = expectedRawData?.toMap(),
748+
expectedData = expectedData,
749+
expectedErrors = expectedErrors,
750+
)
775751

776752
fun DataConnectUntypedData.shouldHaveDataAndErrors(
777753
expectedData: Map<String, Any?>,

0 commit comments

Comments
 (0)